Детали реализации процесса активации Document ID в Sharepoint – часть 2

Это вторая часть серии статей о внутренних деталях реализации процесса активации функциональности “Document ID Service” в Sharepoint, которая добавляет уникальный идентификатор к документам, хранящимся в библиотеках документов сайта, в котором происходит активация. Первая часть доступна по этому адресу. В первой части мы закончили на том, что необходимо подождать как минимум 0,5 часа или изменить системное время прежде, чем запускать задание “Document ID enable/disable”. Давайте продолжим исследование и посмотрим, что происходит дальше.

После активации фичи “Document ID Service”, в настройках сайт коллекции появляется новая ссылка на страницу с настройками Document ID. Если открыть ее сразу после активации, она будет выглядеть следующим образом:

Как было установлено в первой части, теперь мы должны временно изменить системное время: добавим 1 час к текущему времени и перезапустим службу таймера Sharepoint. После этого выбираем задание CA > Monitoring > Job definitions > “Document ID enable/disable” для соответствующего веб-приложения и запускаем его. Если после этого проверить исполняемые задания мы увидим индикатор прогресса задания «Document ID enable/disable» (если бы мы запустили задание, не изменяя системное время, то в истории исполненных заданий мы бы лишь увидели, что задание «Document ID enable/disable» работало 00:00:00, т.е. завершило работу сразу после начала):

После того, как задание завершит свою работу, идем в SQL Management Studio и попробуем выполнить Sql запрос, который мы использовали в первой части. Он не должен вернуть никаких строк, что означает, что рабочий элемент был удален из таблицы WorkItems:

declare @enable uniqueidentifier
set @enable = cast('749FED41-4F86-4277-8ECE-289FBF18884F' as uniqueidentifier)
select * from [dbo].[ScheduledWorkItems] where [Type] = @enable

На этот момент если пойти на страницу настроек Document ID, она будет выглядеть следующим образом:

Т.е. теперь в поле «Begin IDs with the following characters» установлено значение (случайный набор букв и цифр), а также показывается следующее предупреждение:

Configuration of the Document ID feature is scheduled to be completed by an automated process.

Это означает, что процесс активации до сих пор не завершен. Это происходит потому, что задание “Document ID enable/disable” создает другое рабочий элемент с типом A83644F5-78DB-4F95-9A9D-25238862048C. Для того, чтобы проверить это утверждение, исполним следующий Sql запрос:

declare @assign uniqueidentifier
set @assign = cast('A83644F5-78DB-4F95-9A9D-25238862048C' as uniqueidentifier)
select * from [dbo].[ScheduledWorkItems] where [Type] = @assign

Этот рабочий элемент запускается со следующими параметрами, которые указываются в столбце TextPayload таблицы ScheduledWorkItems:

<?xml version="1.0" encoding="utf-16"?>
<DocIdWorkitem xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <fOverwriteExisting>false</fOverwriteExisting>
  <Cookie>1</Cookie>
</DocIdWorkitem>

Этот рабочий элемент обрабатывается заданием «Document ID assignment». Как указано выше, он добавляется заданием «Document ID enable/disable» и устанавливает значения в поле Document ID для всех существующих документов на сайте. Здесь также следует обратить внимание на значение в столбце DeliveryDate. Оно должно быть близким ко времени запуска задания «Document ID enable/disable» на предыдущем шаге. Т.к. мы еще не возвращали системное время назад к правильному значению, для обработки этого элемента нам нужно лишь запустить задание “Document ID assignment” в Central Administration. После этого мы должны увидеть индикатор прогресса этого задания:

и после некоторого времени рабочий элемент будет удален из таблицы ScheduledWorkItems (можете проверить с помощью Sql запроса, который мы использовали выше).

После этого можно вернуть системное время назад и перезапустить службу таймера. Теперь на странице настроек Document ID не должно быть предупреждения. Но поле «Begin IDs with the following characters» по-прежнему будет содержать значение из случайных букв и цифр:

Если вам нужно изменить префикс Document ID на собственное значение, необходимо пройти процесс с начала. После того, как вы нажмете Ok, будет создан рабочий элемент типа 749FED41-4F86-4277-8ECE-289FBF18884F со следующими параметрами:

<?xml version="1.0" encoding="utf-16"?>
<DocIdEnableWorkitem xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <fEnable>true</fEnable>
  <fScheduleAssignment>true</fScheduleAssignment>
  <fOverwriteExisting>false</fOverwriteExisting>
  <fDocsetCtUpdateOnly>false</fDocsetCtUpdateOnly>
  <prefix>TEST</prefix>
</DocIdEnableWorkitem>

(В этом примере используется префикс TEST). А также рабочий элемент типа A83644F5-78DB-4F95-9A9D-25238862048C с параметрами:

<?xml version="1.0" encoding="utf-16"?>
<DocIdWorkitem xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <fOverwriteExisting>false</fOverwriteExisting>
  <Cookie>2</Cookie>
</DocIdWorkitem>

После того, как оба задания будут завершены, для тестирования добавьте любой документ в библиотеку документов. На форме просмотра свойств документа должно быть поле Document ID – ссылка с текстом “TEST-1-1” и адресом http://example.com/_layouts/15/DocIdRedir.aspx?ID=TEST-1-1:

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

Рубрика: Document management, Sharepoint | Оставить комментарий

Детали реализации процесса активации Document ID в Sharepoint – часть 1

При активации фичи Document ID Service на уровне сайт коллекции Sharepoint добавляется поле Document ID в стандартный тип содержимого Документ. Основная проблема это то, что активация происходит асинхронно с помощью заданий таймера один раз в день (см. ниже), что вызывает проблемы в разработчиков и администраторов. На Sharepoint Online ускорить процесс проблематично, но в собственном окружении это возможно и в этой статье я покажу как это сделать и какие механизмы при этом используются. Добавим также, что на Sharepoint Online указанная фича доступна только при использовании Enterprise плана.

Прежде чем продолжать читать эту статью я настоятельно рекомендую ознакомится со следующим моим постом (пост на английском): Internal details of SPWorkItemJobDefinition or several reasons of why SPWorkItemJobDefinition doesn’t work. В нем описаны внутренние детали реализации рабочих элементов (work items) в Sharepoint, которые активно используются при активации Document ID Service. Так что мы должны хорошо понимать, как они работают прежде чем продолжить изучение фичи Document ID.

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

Название: Document ID enable/disable
Тип: Microsoft.Office.DocumentManagement.Internal.DocIdEnableWorkItemJobDefinition
Тип рабочего элемента: 749FED41-4F86-4277-8ECE-289FBF18884F
Описание: Рабочий элемент, который устанавливает изменения в тип содержимого во всех сайтах при изменении конфигурации Document ID
Расписание по умолчанию: Каждый день 21:30-21:45

Название: Document ID assignment
Тип: Microsoft.Office.DocumentManagement.Internal.DocIdWorkItemJobDefinition
Тип рабочего элемента: A83644F5-78DB-4F95-9A9D-25238862048C
Описание: Рабочий элемент, который присваивает Document ID ко всем элементам в сайт коллекции
Расписание по умолчанию: Каждый день 22:00-22:30

Оба задания устанавливаются при активации фичи DocumentManagement (Id = «3A4CE811-6FE0-4e97-A6AE-675470282CF2») со скопом WebApplication. Первое задание добавляет поля в типы содержимого, второе – устанавливает значение для указанных документов. Если вы не видите эти задания в Central administration > Monitoring > Job definitions, активируйте DocumentManagement через PowerShell с указанием флага force. Также обратите внимание на типы рабочих элементов (749FED41-4F86-4277-8ECE-289FBF18884F и A83644F5-78DB-4F95-9A9D-25238862048C) – они скоро нам понадобятся.

Прежде всего нам нужно активизировать фичу “Document ID Service” в Настройках сайта > Компоненты сайт коллекции (Site settings > Site collection features). Давайте посмотрим, что происходит в обработчике фичи Microsoft.Office.DocumentManagement.DocIdFeatureReceiver во время активации и деактивации:

public override void FeatureActivated(SPFeatureReceiverProperties receiverProperties)
{
    FeatureActivateDeactivate(receiverProperties, true);
}
 
public override void FeatureDeactivating(SPFeatureReceiverProperties receiverProperties)
{
    FeatureActivateDeactivate(receiverProperties, false);
}

Внутренний метод FeatureActivateDeactivate() показан ниже:

private static void FeatureActivateDeactivate(SPFeatureReceiverProperties
receiverProperties, bool fActivate)
{
    SPSite parent = receiverProperties.Feature.Parent as SPSite;
    ULS.SendTraceTag(0x61326d37, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.High,
        "Document ID/FeatureActivateDeactivate: Entering  with fActivate = {0}",
        new object[] { fActivate });
    DocumentId.EnableAssignment(parent, null, fActivate, fActivate, false, false);
    ULS.SendTraceTag(0x61326d38, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.High,
        "Document ID/FeatureActivateDeactivate: Leaving");
}

Давайте теперь посмотрим на метод DocumentId.EnableAssignment():

public static void EnableAssignment(SPSite site, string prefix, bool fEnable,
bool fScheduleAssignment, bool  fOverwriteExistingIds, bool fDocsetCtUpdateOnly)
{
    if (site == null)
    {
        throw new ArgumentNullException("site");
    }
    if ((site.RootWeb == null) || (site.RootWeb.ContentTypes == null))
    {
        throw new ArgumentException("site.RootWeb.ContentTypes");
    }
    if (fEnable && !IsFeatureEnabled(site))
    {
        throw new InvalidOperationException(
"Assignment cannot be enabled with the feature deactivated");
    }
    if (DocIdHelpers.IsSiteTooBig(site))
    {
        DocIdEnableWorkitem.ScheduleWorkitem(site, prefix, fEnable,
            fScheduleAssignment, fOverwriteExistingIds, fDocsetCtUpdateOnly);
    }
    else
    {
        DocIdEnableWorkItemJobDefinition.EnableAssignment(site, prefix, fEnable,
            fScheduleAssignment, fOverwriteExistingIds,  fDocsetCtUpdateOnly);
    }
    new DocIdUiSettings(fEnable, prefix).Save(site);
}

Итак сначала проверяется, является ли текущая сайт коллекция “слишком большой”, при помощи метода DocIdHelpers.IsSiteTooBig(). Если да, сначала создается рабочий элемент для добавления полей в типы содержимого и затем второй рабочий элемент для установки значения в добавленные поля для существующих документов (см. ниже). Если сайт не является “слишком большим”, изменения типов содержимого будут произведены синхронно и затем также будет создан рабочий элемент для заполнения полей существующих документов. Т.е. второй рабочий элемент будет создан в любом случае, но мы вернемся к нему позже.

Теперь давайте посмотрим что значит “сайт слишком большой”. Вот как реализована функция DocIdHelpers.IsSiteTooBig():

public static bool IsSiteTooBig(SPSite site)
{
    return IsSiteTooBig(site, 1, 40, 20);
}
 
public static bool IsSiteTooBig(SPSite site, int maxWebs, int maxListsPerWeb, int maxDoclibsPerWeb)
{
    ...
}

Т.е. сайт является “слишком большим”, когда в нем содержится более 1 под сайта, более 40 списков или более 20 библиотек документов. Если одно из этих условий истинно для вашего сайте, типы содержимого будут изменятся асинхронно с помощью рабочего элемента. Как было показано выше, он создается внутри метода DocIdEnableWorkitem.ScheduleWorkitem():

public static void ScheduleWorkitem(SPSite site, string Prefix, bool Enable, bool ScheduleAssignment, bool OverwriteExisting, bool DocsetCtUpdateOnly)
{
    if (site == null)
    {
        throw new ArgumentNullException("site");
    }
    string url = site.Url;
    ULS.SendTraceTag(0x636e7037, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.Medium,
"Document ID Enable:ScheduleWorkitem for '{0}':enable={1},schedule={2},overwrite={3}",
        new object[] { url, Enable, ScheduleAssignment, OverwriteExisting });
    ULS.SendTraceTag(0x636e7038, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.Medium,
"Document ID Enable: ScheduleWorkitem called for site '{0}' by this stack: {1}",
        new object[] { url, Environment.StackTrace });
    XmlSerializer serializer = new XmlSerializer(typeof(DocIdEnableWorkitem));
    StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
    DocIdEnableWorkitem o = new DocIdEnableWorkitem();
    o.fEnable = Enable;
    o.fScheduleAssignment = ScheduleAssignment;
    o.fOverwriteExisting = OverwriteExisting;
    o.fDocsetCtUpdateOnly = DocsetCtUpdateOnly;
    o.prefix = Prefix;
    serializer.Serialize((TextWriter) writer, o);
    string strTextPayload = writer.ToString();
    writer.Dispose();
    site.AddWorkItem(Guid.Empty, DateTime.Now.AddMinutes(30.0).ToUniversalTime(), 
DocIdEnableWorkItemJobDefinition.DocIdEnableWIType, site.RootWeb.ID, site.ID, 1,
false, Guid.Empty, Guid.Empty, site.RootWeb.CurrentUser.ID, null, strTextPayload,
Guid.Empty);
    ULS.SendTraceTag(0x636e7039, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.Medium,
"Document ID Enable: ScheduleWorkitem for '{0}' leaving", new object[] { url });
}

Рабочий элемент добавляется в строках 25-28 с помощью вызова метода SPSite.AddWorkItem(). При этом добавляется рабочий элемент с типом 749FED41-4F86-4277-8ECE-289FBF18884F, который обрабатывается заданием “Document ID enable/disable” (см. выше). Этот элемент добавляет поле Document ID в типы содержимого. Более точно, он добавляет 2 поля: Document ID (SPFieldUrlValue) и Document ID Value (string). Эти поля не отображаются ни на странице просмотра деталей стандартного типа содержимого Документ (Настройки сайта > Типы содержимого) ни в списке столбцов в библиотеках Документы. Вы увидите это поле только после того, как добавите какой-либо документ в библиотеку Документы (или любую другую библиотеку, в которой используется стандартный тип содержимого Документ или кастомный тип содержимого, наследующий стандартный Документ) и затем перейдете на форму просмотра свойств документа. Поле Document ID будет содержать значение и будет работать как ссылка, ведущая на http://example.com/_layouts/15/DocIdRedir.aspx?ID={docId}. Эта ссылка является глобальной и уникальной для документа и будет работать даже после перемещения документа, что является большим преимуществом по сравнению с обычным адресом документа.

Если вы прочитали статью, которую я рекомендовал вначале, вы уже знаете, что рабочие элементы хранятся в таблице ScheduledWorkItems контентной базы данных. Для того, чтобы посмотреть их, запустите следующий Sql запрос:

declare @enable uniqueidentifier
set @enable = cast('749FED41-4F86-4277-8ECE-289FBF18884F' as uniqueidentifier)
select * from [dbo].[ScheduledWorkItems] where [Type] = @enable

Запрос должен вернуть 1 строку:

Этот элемент был создан со следующими параметрами, которые указаны в столбце TextPayload:

<?xml version="1.0" encoding="utf-16"?>
<DocIdEnableWorkitem xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <fEnable>true</fEnable>
  <fScheduleAssignment>true</fScheduleAssignment>
  <fOverwriteExisting>false</fOverwriteExisting>
  <fDocsetCtUpdateOnly>false</fDocsetCtUpdateOnly>
</DocIdEnableWorkitem>

Обратите внимание на столбец DeliveryDate. Он содержит самое раннее время в формате UTC (добавьте 3 часа для того, чтобы добавить локальное время), когда задание может быть обработано. Но если вы попробуете запустить задание вручную в Central administration раньше указанного времени, ничего не произойдет. Этот момент является наиболее частой причиной непонимания: администратор пытается запустить задания сразу после активации фичи. Но как мы видели в листинге метода DocIdEnableWorkitem.ScheduleWorkitem() в столбец DeliverDate записывается значение DateTime.Now.AddMinutes(30.0).ToUniversalTime(). Это означает, что мы должны подождать хотя бы 30 минут (либо изменить системное время), перезапустить службу таймера, запустить задание и затем изменить системное время назад и перезапустить таймер снова (более точно, перед тем как вернуть системное время назад, нужно также запустить задание “Document ID assignment”, как раз после того, как задание “Document ID enable/disable” будет завершено – см. вторую часть для подробностей).

Причина такого поведения в хранимой процедуре [dbo].[proc_GetRunnableWorkItems]:

CREATE PROCEDURE [dbo].[proc_GetRunnableWorkItems] (
        @ProcessingId          uniqueidentifier,
        @SiteId                uniqueidentifier,
        @WorkItemType          uniqueidentifier,
        @BatchId               uniqueidentifier,
        @MaxFetchSize          int = 1000,
        @ThrottleThreshold     int = 0,
        @RequestGuid           uniqueidentifier = NULL OUTPUT     
        )
AS
    SET NOCOUNT ON
    IF (dbo.fn_IsOverQuotaOrWriteLocked(@SiteId) >= 1)
    BEGIN
        RETURN 0
    END
    DECLARE @iRet int
    SET @iRet = 0
    DECLARE @oldTranCount int
    SET @oldTranCount = @@TRANCOUNT
    DECLARE @Now datetime
    SET @Now = dbo.fn_RoundDateToNearestSecond(GETUTCDATE())
    DECLARE @InProgressCount int
    DECLARE @ThrottledFetch int
    DECLARE @ReturnWorkItems bit
    SET @ReturnWorkItems = 0
    BEGIN TRAN
    SET @InProgressCount = 0
    SET @ThrottledFetch = 0
    SET @ThrottleThreshold = @ThrottleThreshold + 1
    IF @ThrottleThreshold > 1
    BEGIN
        SET ROWCOUNT @ThrottleThreshold
        SELECT 
            @InProgressCount = COUNT(DISTINCT BatchId)
        FROM
            dbo.ScheduledWorkItems WITH (NOLOCK)
        WHERE
            Type = @WorkItemType AND
            DeliveryDate <= @Now AND
            (InternalState & (1 | 16)) = (1 | 16)
    END
    IF @BatchId IS NOT NULL
    BEGIN
        SET @ThrottledFetch = 16
    END
    IF @InProgressCount < @ThrottleThreshold
    BEGIN
        UPDATE
            dbo.ScheduledWorkItems
        SET
            InternalState = InternalState | 1 | @ThrottledFetch,
            ProcessingId = @ProcessingId
        FROM
            (
            SELECT TOP(@MaxFetchSize)
                Id
            FROM
                dbo.ScheduledWorkItems
            WHERE
                Type = @WorkItemType AND
                DeliveryDate <= @Now AND
                (@SiteId IS NULL OR 
                    SiteId = @SiteId) AND
                (@BatchId IS NULL OR
                    BatchId = @BatchId) AND
                (InternalState & ((1 | 2))) = 0
            ORDER BY
              DeliveryDate
            ) AS work
        WHERE
            ScheduledWorkItems.Id = work.Id
        SET @InProgressCount = @@ROWCOUNT
        SET ROWCOUNT 0            
        IF @InProgressCount <> 0
        BEGIN
          EXEC @iRet = proc_AddFailOver @ProcessingId, NULL, NULL, 20, 0
        END
        SET @ReturnWorkItems = 1
    END
CLEANUP:
        SET ROWCOUNT 0
        IF @iRet <> 0
        BEGIN
            IF @@TRANCOUNT = @oldTranCount + 1
            BEGIN
                ROLLBACK TRAN
            END
        END
        ELSE
        BEGIN
            COMMIT TRAN
            IF @InProgressCount <> 0
               AND @InProgressCount <> @MaxFetchSize 
               AND @WorkItemType = 'BDEADF09-C265-11d0-BCED-00A0C90AB50F'
               AND @BatchId IS NOT NULL AND @SiteId IS NOT NULL
            BEGIN
                UPDATE
                    dbo.Workflow
                SET
                    InternalState = InternalState & ~(1024)
                WHERE
                    SiteId = @SiteId AND
                    Id = @BatchId    
            END            
            IF @ReturnWorkItems = 1
            BEGIN
                SELECT ALL
DeliveryDate, Type, ProcessMachineId as SubType, Id,
SiteId, ParentId, ItemId, BatchId, ItemGuid, WebId, UserId, Created,
BinaryPayload, TextPayload,
                    InternalState
                FROM
                    dbo.ScheduledWorkItems
                WHERE
                    Type = @WorkItemType AND
                    DeliveryDate <= @Now AND
                    ProcessingId = @ProcessingId
                ORDER BY
                    DeliveryDate
                IF @@ROWCOUNT <> 0
                BEGIN
                    EXEC @iRet = proc_UpdateFailOver @ProcessingId, NULL, 20
                END
            END
        END
        RETURN @iRet
 
GO

Как видите процедура возвращает элементы, для которых выполняется условие DeliveryDate <= @Now. Вот почему нужно изменять системное время для того, чтобы рабочие элементы были обработаны.

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

Рубрика: Document management, Sharepoint | Оставить комментарий

Релиз Camlex 3.6 и Camlex.Client 1.5

Некоторое время назад вышел очередной релиз версии 3.6 библиотеки с открытым исходным кодом Camlex (а также параллельно вышла версия Camlex.Client 1.5 для управляемых клиентских приложений). Для тех, кто еще не слышал об этом проекте, это библиотека, которая помогает составлять динамические CAML запросы (т.е. запросы, построенные во время исполнения, на основе каких-либо параметров), используя лямбда-выражения C# и fluent-интерфейсы. Множество самых разных примеров использования библиотеки можно найти здесь. Также в моем блоге вы можете найти другие статьи, связанные с проектом.

В новом релизе была добавлена поддержка явного преобразования булевых выражений. Т.е. если в предыдущих версиях необходимо было писать выражения следующим образом:

string caml = Camlex.Query().Where(x => (bool)x["Foo"] == true).ToString();

что в свою очередь производит следующий CAML запрос:

<Where>
  <Eq>
    <FieldRef Name="Foo" />
    <Value Type="Boolean">1</Value>
  </Eq>
</Where>

то теперь их можно написать в более простом и коротком виде без явного сравнения с true или false:

string caml = Camlex.Query().Where(x => (bool)x["Foo"]).ToString();

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

Для того чтобы получить CAML запрос, в котором используется сравнение с false, используйте оператор НЕ (!):

string caml = Camlex.Query().Where(x => !(bool)x["Foo"]).ToString();

который произведет:

<Where>
  <Eq>
    <FieldRef Name="Foo" />
    <Value Type="Boolean">0</Value>
  </Eq>
</Where>

Такой же упрощенный синтаксис доступен в сложных запросах, использующих операции сравнения && и ||:

string caml = Camlex.Query().Where(x => ((bool)x["foo1"] && !(bool)x["foo2"]) ||
    (bool)x["foo3"]).ToString();

что даст нам следующий запрос:

<Where>
  <Or>
    <And>
      <Eq>
        <FieldRef Name="Foo1" />
        <Value Type="Boolean">1</Value>
      </Eq>
      <Eq>
        <FieldRef Name="Foo2" />
        <Value Type="Boolean">0</Value>
      </Eq>
    </And>
    <Eq>
      <FieldRef Name="Foo3" />
      <Value Type="Boolean">1</Value>
    </Eq>
  </Or>
</Where>

Также соответствующее изменение было добавлено в reverse engeneering функционал Camlex-а, который позволяет получить C# представление CAML запроса с использованием Camlex, что является обратным преобразованием по сравнению с примерами, показанными выше. Т.е. если ввести например CAML запрос из последнего примера в бесплатный сервис Camlex online, то вы получите также короткую версию преобразования булевого выражения без явного сравнения с true или false. Сервис Camlex online был создан специально для упрощения использования библиотеки Camlex.Net разработчиками, работавшими до этого с обычными строковыми CAML запросами.

Новую версию можно скачать с сайта проекта на Codeplex либо с из NuGet с помощью следующих команд:

Install-Package Camlex.NET.dll
Install-Package Camlex.Client.dll

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

Рубрика: CAML, Camlex.NET, Sharepoint | Оставить комментарий

Восстанавливаем бэкап сайт коллекции с более поздней версии Sharepoint

Иногда на практике возникает ситуация, когда баг воспроизводится только на продакшене, а не на QA или локальном окружении разработки. Такие проблемы сложнее исследовать. Часто они возникают из-за контента, который добавляется только в продакшен. И если у вас нет доступа к нему (напр. если поддержкой инфраструктуры занимается третья компания), вам потребуется запросить бэкап контентной базы данных или сайт коллекции, восстановить его локально и попробовать воспроизвести проблему. Но что делать, если у вас локально установлена более ранняя версия Sharepoint, чем на продакшене? Конечно, лучше иметь такую же версию (имеется ввиду соответствии в установленных кумулятивных обновлений в пределах одной основной версии 14 или 15). В этой статье я покажу трюк, как можно восстановить бэкап с более поздней версии. Прежде чем продолжить, хочу предупредить, что по сути это хак, и нет гарантий того, что в вашем конкретном случае он сработает. В зависимости от версии схема данных Sharepoint может измениться, поэтому стандартные механизмы не поддерживают этот способ. Хотя на практике в большинстве случаев он должен работать.

Хорошо, предположим, что у нас есть бэкап сайт коллекции с продакшена, полученный с помощью командлета Backup-SPSite:

Backup-SPSite http://example1.com -Path C:\Backup\example1.bak

Мы копируем его на локальное окружение и пытаемся восстановить с помощью Restore-SPSite:

Restore-SPSite http://example2.com -Path C:\Backup\example1.bak –Confirm:$false

(я специально использовал разные урлы для source и target сайт коллекций, чтобы показать, что с помощью указанных командлетов можно восстанавливать бэкап в другую сайт коллекцию, т.е. необязательно настраивать идентичное окружение, изменяя hosts файл или local dns). Если у нас установлена более ранняя версия Sharepoint, команда завершится с ошибкой, показав непонятное <nativehr> исключение, которое ничего не скажет о причинах ошибки. Но если переключить мониторинг в режим Verbose, то мы обнаружим следующее сообщение в логе:

Could not deserialize site from C:\Backup\example1.bak. Microsoft.SharePoint.SPException: Schema version of backup 15.0.4505.1005 does not match current schema version 15.0.4420.1017.

(конкретные номера версий не важны. Для данной статьи важно лишь, что версия Sharepoint на текущем окружении 15.0.4420.1017 меньше, чем версия, с которой мы взяли бэкап 15.0.4505.1005).

Что можно сделать в этом случае? Mount-SPContentDatabase не сработает по той же причине, т.е. бэкап контентной базы не поможет. Мы можем либо обновить локальное окружение (и вы должны рассматривать этот вариант как основной), либо пойти по нестандартному пути. Для последнего нам понадобится hex-редактор. Сначала я предположил, что бэкап сайт коллекции, выполненный показанным выше способом, является cab-файлом, который можно будет распаковать, подредактировать текстовые файлы внутри и запаковать обратно (подобный трюк я описывал в статье Сохранение идентичности объектов при экспорте и импорте сайтов в Sharepoint в различные позиции в топологии сайт коллекции). Но это оказалось не так в случае с бэкапом сайт коллекции – они похожи на обычные бинарные файлы. Поэтому для работы с ним нам понадобится hex-редактор. Я использовал HxD, но вы можете использовать любой на ваш вкус.

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

image

Нужно изменить номер версии на тот, который используется на продакшене:

image

Сохраняем файл и запускаем Restore-SPSite снова. Теперь восстановление должно пройти успешно.

Надеюсь, что этот трюк поможет кому-нибудь. Помните только, что это хак, и его нужно использовать осторожно.

Рубрика: Content deployment, Sharepoint | Оставить комментарий

Проблема с обрезанными заголовками на странице результатов поиска в Sharepoint 2013

Во время разработки search-driven сайта на Sharepoint 2013 мы столкнулись со следующей проблемой: для некоторых страниц заголовки были обрезаны в результатах поиска:

image

Т.е. для большинства страниц заголовки отображались нормально, но для некоторых они обрезались, как показано на примере выше (“Find the right solution” –> “he right”, “We as business partner” –> “as busine”, …). Эти заголовки не совсем такие, как в реальном сайте, но они хорошо иллюстрируют проблему. При этом не было очевидного общего признака для страниц, заголовки которых обрезались: для некоторых страниц отображалось несколько полных слов из заголовка, для других слова были обрезаны и ключевые слова, использованные для поиска не показывались.

Исследование показало, что проблема в стандартном шаблоне отображения (display template) Item_CommonItem_Body.html. Заголовок отображается с помощью следующего кода:

var title = Srch.U.getHighlightedProperty(id, ctx.CurrentItem, "Title");
...
var titleHtml = String.format(
'<a clicktype="{0}" id="{1}" href="{2}" class="ms-srch-item-link" ' +
'title="{3}" onfocus="{4}" {5}>{6}</a>',
$htmlEncode(clickType), $htmlEncode(id + Srch.U.Ids.titleLink), $urlHtmlEncode(url),
$htmlEncode(ctx.CurrentItem.Title), showHoverPanelCallback, appAttribs,
Srch.U.trimTitle(title, maxTitleLengthInChars, termsToUse));
...
<div id="_#= $htmlEncode(id + Srch.U.Ids.title) =#_" class="ms-srch-item-title"> 
  <h3 class="ms-srch-ellipsis">
    _#= titleHtml =#_
  </h3>
</div>

Т.е. сначала получаем выделенные заголовки с помощью метода Srch.U.getHighlightedProperty() (строка 1), и затем делаем тримминг для добавления в ссылку с помощью метода Srch.U.trimTitle() (строка 8). Сначала я подумал, что метод trimTitle работает неправильно по какой-то причине. Он, как и метод getHighlightedProperty, определен в файле Search.ClientControls.js, расположенном в папке /15/Template/Layouts. Я добавил в Item_CommonItem_Body.html временные трейсы, которые показали, что обрезанные заголовки возвращаются из getHighlightedProperty. Вот код этого метода:

Srch.U.getHighlightedProperty =
function Srch_U$getHighlightedProperty(key, result, property) {
    var $v_0 = null;
    if (!Srch.U.e(key)) {
        if (!((key) in Srch.ScriptApplicationManager.get_current().$23_1)) {
            Srch.ScriptApplicationManager.get_current().$23_1[key] =
                Srch.U.$5H(result['HitHighlightedProperties']);
        }
        var $v_1 = Srch.ScriptApplicationManager.get_current().$23_1[key];
        if (!Srch.U.n($v_1)) {
            if (property === 'Title') {
                property = 'HHTitle';
            }
            else if (property === 'Path') {
                property = 'HHUrl';
            }
            else {
                property = property.toLowerCase();
            }
            $v_0 = $v_1[property];
        }
    }
    return $v_0;
}

Параметр key содержит идентификатор элемента, который отображается в результатах поиска, result – ctx.CurrentItem, и property в нашем случае содержит “Title”. Исследование показало, что он доходит до строки 12 (property = ‘HHTitle’) и затем читает свойство “HHTitle” из объекта, который возвращается из следующего вызова: Srch.U.$5H(result[‘HitHighlightedProperties’]) (строки 6-7). Т.е. проблема вызвана неправильным вычислением выделенных заголовков, записанных в ctx.CurrentItem[‘HitHighlightedProperties’].

Тесты также показали, что языковые установки браузера влияют на результат, т.е. заголовки могут обрезаться для одного языка, но показываться правильно для другого. В качестве обходного пути для этой проблемы, можно скопировать шаблон отображения Item_CommonItem_Body.html и изменить его код на следующий:

'<a clicktype="{0}" id="{1}" href="{2}" class="ms-srch-item-link" ' +
'title="{3}" onfocus="{4}" {5}>{3}</a>',
$htmlEncode(clickType), $htmlEncode(id + Srch.U.Ids.titleLink), $urlHtmlEncode(url),
$htmlEncode(ctx.CurrentItem.Title), showHoverPanelCallback, appAttribs);

Т.е. использовать $htmlEncode(ctx.CurrentItem.Title), который возвращает исходный неизмененный заголовок, вместо выделенного. После этого надо указать этот шаблон отображения в свойствах веб части ResultScriptWebPart, которая отображает результаты поиска. Но с этим решением ключевые слова, использованные для поиска, не будут выделяться в заголовках.

Описанная проблема выглядит как баг в компонентах поиска. Если вы столкнулись с ней или нашли другое решение, то опишите его в комментариях.

Рубрика: Search, Sharepoint | Оставить комментарий

Поддержка операции IN в Camlex 3.5 и Camlex.Client 1.3

Этим летом я выпустил новые версии проектов с открытым исходным кодом: Camlex и Camlex.Client (на момент написания этой статьи, был выпущен еще один релиз для клиентской версии Camlex-а 1.4, однако здесь речь пойдет об изменениях, добавленных в версии 1.3. Версия основного Camlex-а не изменилась, т.е. мы будем говорить о Camlex 3.5). Сначала я покажу несколько примеров использования, а затем скажу несколько слов об использовании Camlex в разных версиях Sharepoint.

Что такое операция IN? Вот что говорит msdn по этому поводу:

Определяет, равно ли значение элемента списка для поля, заданное элементом FieldRef, одному из значений, заданных элементом Values.

Например, если нам надо получить список элементов, у которых целочисленное поле содержит значение в интервале [0..9], то мы можем использовать следующий запрос:

<br>&lt;Where&gt;<br>&lt;In&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Values&gt;<br>&lt;Value Type="Integer"&gt;0&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;1&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;2&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;3&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;4&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;5&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;6&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;7&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;8&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;9&lt;/Value&gt;<br>&lt;/Values&gt;<br>&lt;/In&gt;<br>&lt;/Where&gt;<br>

В предыдущих версиях Camlex для получения такого же результата можно было скомбинировать несколько Eq (==) условий с помощью операции Or. Camlex специально проектировался для выполнения таких задач, для упрощения создания динамических CAML запросов, так что решить задачу можно очень просто одной строкой:

<br>string caml = Camlex.Query().WhereAny(<br>Enumerable.Range(0, 9).Select&lt;int, Expression&lt;Func&lt;SPListItem, bool&gt;&gt;&gt;(<br>i =&gt; x =&gt; (int) x["Count"] == i)).ToString();<br>

Давайте посмотрим на результат выполнения этого кода:

<br>&lt;Where&gt;<br>&lt;Or&gt;<br>&lt;Or&gt;<br>&lt;Or&gt;<br>&lt;Or&gt;<br>&lt;Or&gt;<br>&lt;Or&gt;<br>&lt;Or&gt;<br>&lt;Or&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;0&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;1&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;/Or&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;2&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;/Or&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;3&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;/Or&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;4&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;/Or&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;5&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;/Or&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;6&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;/Or&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;7&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;/Or&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;8&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;/Or&gt;<br>&lt;/Where&gt;<br>

Сравнивая с первым примером, выглядит не очень красиво. Интервал значения может быть и больше, в этом случае запрос будет еще более развернутым. Однако теперь, с добавлением поддержки операции IN, можно написать запрос, используя следующий синтаксис:

<br>string caml =<br>Camlex.Query().Where(x =&gt; Enumerable.Range(0, 9).Contains((int)x["Count"]))<br>.ToString();<br>

Он создаст CAML запрос, который мы уже видели:

<br>&lt;Where&gt;<br>&lt;In&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Values&gt;<br>&lt;Value Type="Integer"&gt;0&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;1&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;2&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;3&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;4&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;5&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;6&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;7&lt;/Value&gt;<br>&lt;Value Type="Integer"&gt;8&lt;/Value&gt;<br>&lt;/Values&gt;<br>&lt;/In&gt;<br>&lt;/Where&gt;<br>

Для тех, кто работал с NHibernate или Linq 2 Sql, такой синтаксис не будет новым: эти фреймворки используют похожий прием для генерации Sql запросов с операцией IN. Я специально использовал пример с динамически наполняемым массивом значений (Enumerable.Range(0, 9)), т.к. хотел показать, что код будет работать с любым выражением или функцией, возвращающей IEnumerable:

<br>var caml = Camlex.Query().Where(x =&gt; getArray().Contains((int)x["Count"])).ToString();<br>...<br>List&lt;int&gt; getArray()<br>{<br>var list = new List&lt;int&gt;();<br>for (int i = 0; i &lt; 10; i++)<br>{<br>list.Add(i);<br>}<br>return list;<br>}<br>

(Вы видите разницу между последним примером, в котором функция возвращает List<int>, и предыдущим примером, который использует IEnumerable? Если да, то вы хорошо знаете C#🙂 Класс List<T> имеет свой Contains метод, тогда как для IEnumerable<int> из первого примера, используется метод расширения. Как видно, Camlex может работать с обоими. Если вы используете Linq, не забудьте добавить “using System.Linq;” в начало вашего cs файла).

Т.е. основной синтаксис для новой функциональности следующий:

<br>var caml = Camlex.Query().Where(x =&gt;<br>enumerable.Contains((Type)x["FieldTitle"])).ToString();<br>

Конечно, также вы можете использовать массив с константами:

<br>string c = Camlex.Query().Where(x =&gt; new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}<br>.Contains((int) x["Count"])).ToString();<br>

Также он будет работать с любыми типами для элемента Value:

<br>string c = Camlex.Query().Where(x =&gt; new[]<br>{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}<br>.Contains((string) x["Title"])).ToString();<br>

Этот пример создаст следующий CAML:

<br>&lt;Where&gt;<br>&lt;In&gt;<br>&lt;FieldRef Name="Title" /&gt;<br>&lt;Values&gt;<br>&lt;Value Type="Text"&gt;zero&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;one&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;two&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;three&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;four&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;five&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;six&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;seven&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;eight&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;nine&lt;/Value&gt;<br>&lt;/Values&gt;<br>&lt;/In&gt;<br>&lt;/Where&gt;<br>

Как видите, теперь он использует Text тип в элементах Value.

Это была только половина истории. Если вы следите за Camlex-ом, то возможно помните, что начиная с версии 3.0 он стал двунаправленным (см. здесь: Camlex.NET became bidirectional and goes online. Version 3.0 is released). Так что преобразования были добавлены также в обе стороны: из дерева выражений (expression tree) в CAML и из CAML в дерево выражений. Т.е. можно взять CAML запрос в строковой переменной, и получить дерево выражений из него:

<br>var xml =<br>"&lt;Query&gt;" +<br>" &lt;Where&gt;" +<br>" &lt;In&gt;" +<br>" &lt;FieldRef Name=\"Title\" /&gt;" +<br>" &lt;Values&gt;" +<br>" &lt;Value Type=\"Text\"&gt;zero&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;one&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;two&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;three&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;four&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;five&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;six&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;seven&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;eight&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;nine&lt;/Value&gt;" +<br>" &lt;/Values&gt;" +<br>" &lt;/In&gt;" +<br>" &lt;/Where&gt;" +<br>"&lt;/Query&gt;";<br><br>var expr = Camlex.QueryFromString(xml).ToExpression();<br>

Эта возможность дает много интересных приложений:

1. на http://camlex-online.org/ добавьте xml, показанный выше в поле ввода и нажмите Convert to C#. Он покажет вам, как такой запрос можно написать на Camlex-е:

<br>Camlex.Query().Where(x =&gt; new[] {<br>"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }<br>.Contains((string)x["Title"]))<br>

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

<br>var xml =<br>"&lt;Query&gt;" +<br>" &lt;Where&gt;" +<br>" &lt;In&gt;" +<br>" &lt;FieldRef Name=\"Title\" /&gt;" +<br>" &lt;Values&gt;" +<br>" &lt;Value Type=\"Text\"&gt;one&lt;/Value&gt;" +<br>" &lt;Value Type=\"Text\"&gt;two&lt;/Value&gt;" +<br>" &lt;/Values&gt;" +<br>" &lt;/In&gt;" +<br>" &lt;/Where&gt;" +<br>"&lt;/Query&gt;";<br><br>string caml = Camlex.Query().WhereAll(xml, x =&gt; (int)x["Count"] == 1).ToString();<br>

произведет следующий CAML:

<br>&lt;Where&gt;<br>&lt;And&gt;<br>&lt;Eq&gt;<br>&lt;FieldRef Name="Count" /&gt;<br>&lt;Value Type="Integer"&gt;1&lt;/Value&gt;<br>&lt;/Eq&gt;<br>&lt;In&gt;<br>&lt;FieldRef Name="Title" /&gt;<br>&lt;Values&gt;<br>&lt;Value Type="Text"&gt;one&lt;/Value&gt;<br>&lt;Value Type="Text"&gt;two&lt;/Value&gt;<br>&lt;/Values&gt;<br>&lt;/In&gt;<br>&lt;/And&gt;<br>&lt;/Where&gt;<br>

Кроме комбинирования строковых запросов с запросами на лямбдах, Camlex может соединять несколько строковых запросов с разными условиями в один результирующий запрос.

Это была часть с примерами. Теперь несколько слов про использования Camlex в различных версиях Sharepoint. Изначально Camlex был создан для Sharepoint 2007 и он использует ссылку на Microsoft.SharePoint.dll версии 12.0.0.0 (он использует ссылку только для компиляции. Camlex не вызывает никаких методов Sharepoint API). Благодаря автоматическому перенаправлению биндинга сборок в Sharepoint (assembly binding redirect, про который я писал здесь: Assembly binding redirect in Sharepoint 2010: how old code for SP 2007 works in SP 2010) он также работает в версиях 2010 и 2013. Операция IN была добавлена в схему CAML в Sharepoint 2010, т.е. запросы с ней не будут работать в Sharepoint 2007. Так что перед релизом версии 3.5 мне нужно было сделать выбор между несколькими путями дальнейшего развития Camlex:

1. добавить новую функциональность в существующую ветку Camlex и продолжать использовать Microsoft.SharePoint.dll версии 12.0.0.0. Преимущество: одна сборка работает со всеми версиями Sharepoint, что проще для разработчиков. Недостаток: элемент IN можно будет также создать в Sharepoint 2007, но по факту такой запрос не будет работать.

2. создать отдельную ветку для Sharepoin 2007 и оставить ссылку на версию 12.0.0.0 только в ней, в то время, как в основной ветке изменить ее на 14.0.0.0, так что ее можно будет использовать только начиная с Sharepoint 2010, где операция IN поддерживается. Преимущества и недостатки подхода, такие же как в 1-м подходе, только в обратном направлении.

Также во втором подходе было бы необходимо поддерживать еще одну ветку (сейчас я поддерживаю 2 ветки: default для основного Camlex и client для клиентской версии. Операция IN добавлена в Camlex.Client в версии 1.3). Так как на текущий момент я единственный активный contributor проекта и время (хотя все чаще приходят pull request-ы, за что большое спасибо сообществу), которое я могу тратить на проект, очень ограничено, я выбрал первый путь (по крайней мере пока). В будущем я не исключаю возможности создать различные ветки, но пока будет одна ветка, из которой будет собираться Camlex для всех версий Sharepoint-а.

С обновлением сборок на Codeplex-е, я также обновил пакеты nuget. Вы можете добавить последнюю версию с помощью следующих команд:

Install-Package Camlex.NET.dll

и для клиентской версии:

Install-Package Camlex.Client.dll

В следующих релизах я планирую добавить оставшиеся новые элементы, добавленные в схему CAML с Sharepoint 2010 (Include, NotInclude) и Join-ы (которые тоже конечно надо добавить наконец).

Рубрика: CAML, Camlex.NET, Sharepoint | Комментарии (11)

Как получить адрес текущей сайт коллекции и другие серверные свойства на клиентской стороне в Sharepoint

Что вы будете делать, если вам понадобится получить адрес текущей сайт коллекции в javascript? Как вы понимаете, location.href для этого использовать нельзя, т.к. имея только адрес текущей страницы, мы не сможем сказать, какая его часть относится к сайту, какая к сайт коллекции (для этого в Sharepoint определены managed paths). Если есть возможность внедрить серверный код, то эта задача может быть решена добавлением следующего кода напрямую в aspx, ascx или master файл:

<script type="text/javascript"> 
var siteCollUrl = '<%= SPContext.Current.Site.Url %>';
</script>

После рендеринга страницы вы получите инициализированную переменную siteCollUrl, в которой будет установлен адрес сайт коллекции. Далее вы сможете использовать ее в javascript коде на странице. Но что если у вас нет доступа к серверному коду? Например, если вы создаете кастомный шаблон отображения (display template) для поиска и единственные доступные инструменты, которые вы можете использовать, это html, css и javascript.

Один из способов – это использовать клиентскую объектную модель, в частности объект ClientContext. В этом случае показанный выше код будет переписан следующим образом:

var siteCollUrl = "";
SP.SOD.executeFunc("SP.js", "SP.ClientContext", function()
{
    var clientContext = new SP.ClientContext.get_current();
    var site = clientContext.get_site();
    clientContext.load(site);
    clientContext.executeQueryAsync(Function.createDelegate(this, function(){
        siteCollUrl = site.get_url();
    }))
});

Прежде всего весь код обернут в SP.SOD.executeFunc() для того, чтобы убедиться, что скрипт с SP.ClientContext загружен и готов к использованию на момент выполнения нашего кода (строка). В этом примере мы получаем объект javascript-аналог серверного класса SPSite, который представляет сайт коллекцию (строка), и затем инициализируем его свойства с помощью вызовов методов SP.ClientContext.load() и SP.ClientContext.executeQueryAsync(). Это в свою очередь приводит к асинхронному вызову Sharepoint end-point-а на серверной стороне (строки).

Этот метод работает, но выглядит сложно. Есть ли более простой способ получить серверные свойства на клиентской стороне? Такие способы есть, и один из них объект _spPageContextInfo. Если вы проверите html код вашей страницы на Sharepoint сайте, вы обнаружите эту переменную, инициализированную следующим образом (предполагаем, что вы находитесь на сайт коллекции с адресом http://example.com/test):

var _spPageContextInfo = {
    webServerRelativeUrl: "\u002ftest",
    webAbsoluteUrl: "http:\u002f\u002fexample\u002ftest",
    siteAbsoluteUrl: "http:\u002f\u002fexample.com\u002ftest",
    serverRequestPath: "\u002f_layouts\u002f15\u002fosssearchresults.aspx",
    layoutsUrl: "_layouts\u002f15",
    webTitle: "Home",
    webTemplate: "53",
    tenantAppVersion: "0",
    webLogoUrl: "_layouts\u002f15\u002fimages\u002fsiteicon.png",
    webLanguage: 1033,
    currentLanguage: 1033,
    currentUICultureName: "en-US",
    currentCultureName: "en-US",
    clientServerTimeDelta: new Date("2013-06-07T19:07:01.3494510Z") - new Date(),
    siteClientTag: "40$$15.0.4420.1017",
    crossDomainPhotosEnabled: false,
    webUIVersion: 15,
    webPermMasks: {
        High: 2147483647,
        Low: 4294967295
    },
    pagePersonalizationScope: 1,
    userId: 1,
    systemUserKey: "i:0\u0029.w|s-1-5-21-...",
    alertsEnabled: false,
    siteServerRelativeUrl: "\u002ftest",
    allowSilverlightPrompt: 'True'
};

Как видно из примера, _spPageContextInfo содержит много полезных свойств таких, как адреса сайта и сайт коллекции, информация о культуре и даже маски разрешений (permission mask). С объектом _spPageContextInfo наш код будет переписан следующим образом:

<script type="text/javascript">
var siteCollUrl = _spPageContextInfo.siteAbsoluteUrl;
</script>

Последний пример выглядит гораздо более привлекательнее, чем с ClientContext. Однако остается вопрос, как именно _spPageContextInfo добавляется на страницу, и где он доступен, а где – нет. Для ответа на этот вопрос необходимо изучить код сборок Sharepoint-а в рефлекторе. Объект _spPageContextInfo регистрируется в приватном методе SharePointClientJs_Register() класса ScriptLink (я решил показать код всего метода здесь, чтобы было понятно, как именно устанавливаются javascript свойства с помощью серверного кода):

private static void SharePointClientJs_Register(Page page)
{
    if (((SPContext.Current != null) && !SPPageContentManager.IsStartupScriptRegistered(page,
typeof(ScriptLink), "pagecontextinfo")) && !SPRibbon.ToolbarRibbonAdapterDataRenderingOnly)
    {
        SPWeb web = SPContext.Current.Web;
        SPSite site = SPContext.Current.Site;
        string scriptLiteralToEncode = string.IsNullOrEmpty(web.SiteLogoUrl) ?
(SPUtility.GetLayoutsFolder(web) + "/images/siteicon.png") : web.SiteLogoUrl;
        StringBuilder sb = new StringBuilder();
        if (web != null)
        {
            PersonalizationScope shared;
            sb.Append("var _fV4UI=");
            sb.Append((web.UIVersion > 3) ? "true;" : "false;");
            sb.Append("var _spPageContextInfo = {webServerRelativeUrl: \"");
            sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(web.ServerRelativeUrl));
            sb.Append("\", webAbsoluteUrl: \"");
            sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(web.Url));
            sb.Append("\", siteAbsoluteUrl: \"");
            sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(web.Site.Url));
            sb.Append("\", serverRequestPath: \"");
            sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(HttpContext.Current.Request.Path));
            sb.Append("\", layoutsUrl: \"");
            sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(SPUtility.ContextLayoutsFolder));
            sb.Append("\", webTitle: \"");
            if (web.DoesUserHavePermissions(SPBasePermissions.EmptyMask | SPBasePermissions.Open))
            {
                sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(web.Title));
            }
            else
            {
                sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(""));
            }
            sb.Append("\", webTemplate: \"");
            if (web.DoesUserHavePermissions(SPBasePermissions.EmptyMask | SPBasePermissions.Open))
            {
                sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(
web.WebTemplateId.ToString(CultureInfo.InvariantCulture)));
            }
            else
            {
                sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(""));
            }
            sb.Append("\", tenantAppVersion: \"");
            sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(
SPTenantAppUtils.GetTenantAppEtag(web)));
            sb.Append("\", webLogoUrl: \"");
            sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(scriptLiteralToEncode));
            sb.Append("\", webLanguage: ");
            sb.Append(web.Language);
            sb.Append(", currentLanguage: ");
            sb.Append(Thread.CurrentThread.CurrentUICulture.LCID);
            sb.Append(", currentUICultureName: \"");
            sb.Append(Thread.CurrentThread.CurrentUICulture.Name);
            sb.Append("\", currentCultureName: \"");
            sb.Append(Thread.CurrentThread.CurrentCulture.Name);
            sb.Append("\", clientServerTimeDelta: new Date(\"");
            sb.Append(DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture));
            sb.Append("\") - new Date()");
            sb.Append(", siteClientTag: \"");
            sb.Append(web.SiteClientTag.ToString(CultureInfo.InvariantCulture));
            sb.Append("\", crossDomainPhotosEnabled:");
            sb.Append(site.WebApplication.CrossDomainPhotosEnabled ? "true" : "false");
            sb.Append(", webUIVersion:");
            sb.Append(web.UIVersion.ToString(CultureInfo.InvariantCulture));
            sb.Append(", webPermMasks:");
            sb.Append(SPUtility.BasePermissionsToJson(web.EffectiveBasePermissions));
            if (SPContext.Current.ListId != Guid.Empty)
            {
                sb.AppendFormat(",pageListId:\"{0}\"", SPContext.Current.ListId.ToString("B"));
            }
            if (SPContext.IsPageDoclibItem)
            {
                sb.AppendFormat(",pageItemId:{0}", SPContext.Current.PageItemId);
            }
            if (web.WebPartManager != null)
            {
                shared = web.WebPartManager.Scope;
            }
            else
            {
                shared = PersonalizationScope.Shared;
            }
            sb.AppendFormat(", pagePersonalizationScope:{0}", (int) shared);
            if (web.CurrentUser != null)
            {
                sb.Append(",userId:");
                sb.Append(web.CurrentUser.ID.ToString(CultureInfo.InvariantCulture));
                sb.AppendFormat(", systemUserKey:\"{0}\"",
SPHttpUtility.EcmaScriptStringLiteralEncode(web.CurrentUser.SystemUserKey));
            }
            if (site != null)
            {
                SPWebApplication webApplication = site.WebApplication;
                sb.Append(", alertsEnabled:");
                if (((webApplication != null) && webApplication.AlertsEnabled) &&
webApplication.IsEmailServerSet)
                {
                    sb.Append("true");
                }
                else
                {
                    sb.Append("false");
                }
                sb.Append(", siteServerRelativeUrl: \"");
                sb.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(site.ServerRelativeUrl));
                sb.AppendFormat("\", allowSilverlightPrompt:'{0}'", site.WebApplication.AllowSilverlightPrompt);
            }
            string themedCssFolderUrl = SPUtility.GetThemedCssFolderUrl();
            if (!string.IsNullOrEmpty(themedCssFolderUrl))
            {
                sb.Append(",\"themedCssFolderUrl\" : \"" + themedCssFolderUrl + "\"");
                StringCollection themedImages = new StringCollection();
                StringCollection strings2 = new StringCollection();
                HashSet<string> addedThemedImages = new HashSet<string>();
                HashSet<string> set2 = new HashSet<string>();
                AddImageToCollection(themedImages, "spcommon.png", addedThemedImages);
                AddImageToCollection(themedImages, "ellipsis.11x11x32.png", addedThemedImages);
                AddImageToCollection(themedImages, "O365BrandSuite.95x30x32.png", addedThemedImages);
                AddImageToCollection(themedImages, "socialcommon.png", addedThemedImages);
                AddImageToCollection(themedImages, "spnav.png", addedThemedImages);
                SPWebPartManager webPartManager = web.WebPartManager;
                WebPartCollection webParts = null;
                if (webPartManager != null)
                {
                    webParts = webPartManager.WebParts;
                }
                if (webParts != null)
                {
                    for (int i = 0; i < webParts.Count; i++)
                    {
                        WebPart part = webParts[i] as WebPart;
                        if (part != null)
                        {
                            AddWebpartThemedImagesToCollection(themedImages,
part.GetThemedImages(), addedThemedImages);
                            AddWebpartThemedImagesToCollection(strings2,
part.GetThemedLocalizedImages(), set2);
                        }
                    }
                }
                GenerateThemedImageJson(themedImages, strings2, sb);
            }
            sb.Append("};");
        }
        SPPageContentManager.RegisterClientScriptBlock(page, typeof(ScriptLink),
"pagecontextinfo", sb.ToString());
    }
}

Анализ использования данного метода показывает, что он вызывается при регистрации на стандартных скриптов init.js и sp.core.js, которые добавляются на страницу с помощью контрола ScriptLink:

internal static void RegisterForControl(Control ctrl, Page page, string name,
bool localizable, bool defer, bool loadAfterUI, string language, bool injectNoDefer,
bool controlRegistration, [Optional, DefaultParameterValue(false)] bool loadInlineLast,
[Optional, DefaultParameterValue(false)] bool ignoreFileNotFound)
{
    ...
    RegisterStringsJsIfNecessary(name, page);
    if ((string.Compare(name, "core.js", StringComparison.OrdinalIgnoreCase) == 0) || (string.Compare(name, "bform.js", StringComparison.OrdinalIgnoreCase) == 0))
    {
        ...
    }
    if (string.Compare(name, "SP.Core.js", StringComparison.OrdinalIgnoreCase) == 0)
    {
        ...
        SharePointClientJs_Register(page);
    }
    else if (string.Compare(name, "init.js", StringComparison.OrdinalIgnoreCase) == 0)
    {
        InitJs_Register(page);
    }
    ...
}

Также в методе InitJs_Register():

private static void InitJs_Register(Page page)
{
    ...
    SharePointClientJs_Register(page);
}

Эти 2 скрипта являются одними из основных клиентских скриптов Sharepoint-а и добавляются практически на все страницы на Sharepoint сайтах (например, по умолчанию присутствуют на publishing и на application layout страницах). Т.е. будет достаточно безопасно использовать объект _spPageContextInfo в вашем коде, определенном для этих страниц. Одна проблема, которую я нашел на текущий момент, связана с тем, что он не определен в Sharepoint приложениях (apps). Тем не менее, это достаточно удобное стандартное средство, которое может помочь вам в повседневной работе и уменьшить количество кода.

Рубрика: Client object model, Sharepoint | Оставить комментарий