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

SharePoint и XSLT: область ссылок «быстрого доступа»

Все вы помните ссылку «Добавить новый элемент» внизу представлений списков SharePoint:
image

В связи с наличием этой ссылки возникает два вопроса:
  1. Как поменять текст этой ссылки? (например вместо “Добавить новый элемент”, я хочу текст “Добавить товар” или “Добавить продукт” и т.д.)
  2. Как добавить еще пару ссылок рядом со ссылкой для создания нового элемента?
Все это можно. Например, некий список с заявками может иметь вот такие ссылки быстрого доступа:

image

, где ссылка Configure application route открывает редактор маршрута заявки (т.е. можно определить, кто и в каком порядке должен одобрять эту заявку).

В этой статье я расскажу, как реализовать такие изменения. Рассмотрим два способа:
  1. no-code — для частных решений
  2. программно – что может потребоваться для создания коробочных решений

No code

Как обычно в случае XSLT, самый простой и быстрый способ найти нужный шаблон и произвести нужные изменения — это использовать SharePoint Designer 2010 (далее SPD). Откройте ваш список в SPD, щелкните по названию нужного представления, и вы увидите визуальный дизайнер страницы.

Однако, ссылки «Add new item» вы не увидите, даже несмотря на то, что на исходной странице в браузере она присутствует.

Чтобы её увидеть, необходимо сделать следующее: щелкнуть куда-нибудь внутрь таблицы, в контекстной ленте в группе вкладок List View Tools щелкнуть Design, и выбрать Options –> Summary Toolbar.

image

Дальше всё очень просто, щелкаем на надпись “Add new item”:

image

, и в правой панели в списке свойств видим:

image

В это свойство можно вписать ваш текст (обратите внимание на апострофы):

image

Жмем Enter, сохраняем страницу (кнопка image сверху слева), переходим в браузер, обновляем страницу с представлением списка, вуаля!

image

Чтобы добавить еще одну ссылку рядом с New memo, желательно всё-таки использовать XSLT-разметку.

Для этого, давайте выделим всю строку, содержащую ссылку New memo. Кстати, вместо того чтобы тыкать 10 раз и пытаться попасть в нужный тэг <tr>, существует простой способ выделить строку, к которой принадлежит текущее выделение:

image

Теперь, не снимая выделения, переключаемся на вкладку Code:

image

Нам будет отображен фрагмент XSL-преобразования, ответственный за отображение как раз той самой строки с ссылкой.

Соответственно, этот кусок можно скопировать, вставить копию чуть ниже, и подкорректировать.

Во-первых, нужно заменить картинку, за которую ответственен вот этот кусок кода:
< span style="height:10px;width:10px;position:relative;display:inline-block;overflow:hidden;" class="s4-clust">

  <img src="/_layouts/images/fgimg.png" alt="" style="left:-0px !important;top:-128px !important;position:absolute;"  />
</span>
Подойдет любая картинка 10х10 пикселов. Если вы берете картинку не из спрайта, код можно упростить примерно до такого вида:

<img src="/_layouts/images/wpedit.gif" alt="" style="height:10px;width:10px;" />
Далее, нужно подкорректировать саму ссылку, за которую ответственен вот этот кусок XSLT-кода:

  <a class="ms-addnew" id="{$ID}"
     href="{$Url}"
     onclick="javascript:NewItem2(event, &quot;{$Url}&quot;);javascript:return false;"
     target="_self">
    <xsl:value-of select="'New memo'" />
  </a>
Здесь совет: когда это возможно, используйте JavaScript, и открывайте модальный диалог с нужной вам страницей. Это более привычное поведение для пользователей, т.к. ссылка New item открывает именно модальный диалог.

В остальных случаях, очень полезно использовать функцию GoToPage, которая описана у Alejo El Norte. Она добавит параметр Source к адресу целевой страницы, и позволит вам по нажатию кнопки «Отмена» или «Назад» на целевой странице, легко вернуться туда, откуда пользователь пришел.

В любом случае, открываете ли вы некую страницу в модальном диалоге или напрямую, как правило для формирования ссылки требуются некоторые типовые сведения:


  • GUID текущего списка
  • Url текущего Web’а
  • и т.д.

  • Все эти сведения можно взять из глобальных параметров XSLT. Они довольно хорошо описаны на MSDN.

    Чтобы не искать, самые интересные параметры:

    $ListGUID списка
    $ListUrlDirАдрес корневого каталога списка, аналог SPList.RootFolder.ServerRelativeUrl
    $ServerRelativeUrlАдрес текущего узла относительно адреса сервера, аналог SPWeb.ServerRelativeUrl
    $RootSiteUrlАдрес коллекции узлов, которой принадлежит текущий узел
    $LCIDСтрока, содержащая номер текущей локали, аналог Thread.CurrentThread.CurrentUICulture.LCID
    $UseridID текущего пользователя

    Также, иногда хочется отобразить ту или иную ссылку по некоему условию. Наиболее частое условие — наличие у пользователя каких-либо прав. В этом случае пригодится функция ddwrt:IfHasRights.

    Например, если я хочу отобразить ссылку на переход на некую страницу с дополнительными настройками списка, только если у пользователя есть права на управление списками ( SPBasePermissions.ManageLists), то я должен обернуть соответствующий <tr> в тег <xsl:if>, и передать в ddwrt:IfHasRights числовое значение, соответствующее ManageLists, т.е. 2048:

    <xsl:if test="ddwrt:IfHasRights(2048)">
      <!-- 2048 = SPBasePermissions.ManageLists -->
      <tr>
        <td class="ms-addnew" style="padding-bottom: 3px">
          <img src="/_layouts/images/wpedit.gif" alt="" style="height:10px;width:10px;" />
          <xsl:text disable-output-escaping="yes" ddwrt:nbsp-preserve="yes">&amp;nbsp;</xsl:text>
          <a class="ms-addnew" href="javascript:GoToPage('{$HttpVDir}/_layouts/My/MyPage.aspx?List={$List}');">
            Advanced settings
          </a>
        </td>
      </tr>
    </xsl:if>

    Соответствие строковых значений перечисления числовым, можно легко узнать, написав в Visual Studio “SPBasePermissions”, и нажав F12.

    Программный способ

    Даже если вы большой любитель всё делать только в Visual Studio, я бы всё равно порекомендовал сгенерить нужный XSLT код с помощью SPD. Используя SPD, вы получаете хорошую возможность оттестировать ваш код, добиться чтобы он работал и выглядел как должно. И главное – в SPD это можно сделать это быстро и минимальными усилиями.

    Имея готовый отлаженный код, перейдите во вкладку Code в SPD, и скопируйте полностью содержимое тега <Xsl>. Этот код вставьте в отдельный xslt-файл, который нужно будет задеплоить куда-нибудь в Layouts.

    Если вы всё-таки не хотите копаться в SPD, но хорошо чувствуете себя с XSLT… Что ж, минималистичный пример преобразования, которое заменяет стандартное “New item” на “New product”, приведен ниже:

    <xsl:stylesheet
      xmlns:x="http://www.w3.org/2001/XMLSchema"
      xmlns:d="http://schemas.microsoft.com/sharepoint/dsp"
      version="1.0"
      exclude-result-prefixes="xsl msxsl ddwrt"
      xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime"
      xmlns:asp="http://schemas.microsoft.com/ASPNET/20"
      xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:msxsl="urn:schemas-microsoft-com:xslt"
      xmlns:SharePoint="Microsoft.SharePoint.WebControls"
      xmlns:ddwrt2="urn:frontpage:internal"
      xmlns:o="urn:schemas-microsoft-com:office:office">
    
      <xsl:include href="/_layouts/xsl/main.xsl" />
      <xsl:include href="/_layouts/xsl/internal.xsl" />
    
      <xsl:template name="Freeform">
        <xsl:param name="AddNewText"/>
        <xsl:param name="ID"/>
        <xsl:variable name="Url" select="$ENCODED_FORM_NEW"/>
    
        <xsl:if test="$ListRight_AddListItems = '1' and (not($InlineEdit) or $IsDocLib)">
          <table id="Hero-{$WPQ}" width="100%" cellpadding="0" cellspacing="0" border="0">
            <tr>
              <td colspan="2" class="ms-partline">
                <img src="/_layouts/images/blank.gif" width="1" height="1" alt="" />
              </td>
            </tr>
            <tr>
              <td class="ms-addnew">
                <span style="height:10px;width:10px;position:relative;display:inline-block;overflow:hidden;" class="s4-clust">
                  <img src="/_layouts/images/fgimg.png" alt="" style="left:-0px !important;top:-128px !important;position:absolute;"  />
                </span>
                <xsl:text disable-output-escaping="yes" ddwrt:nbsp-preserve="yes">&amp;nbsp;</xsl:text>
                <a class="ms-addnew" id="{$ID}"
                    href="{$Url}"
                    onclick="javascript:NewItem2(event, &quot;{$Url}&quot;);javascript:return false;"
                    target="_self">
                  <xsl:value-of select="New memo" />
                </a>
              </td>
            </tr>
            <tr>
              <td>
                <img src="/_layouts/images/blank.gif" width="1" height="5" alt="" />
              </td>
            </tr>
          </table>
        </xsl:if>
      </xsl:template>
    
    </xsl:stylesheet>

    Как и в случае с SDP, этот код нужно положить в отдельный файл, и задеплоить куда-нибудь в Layouts.

    Файл есть, теперь XSLT-преобразование к представлению списка подключаются очень просто, например при активации вашей фичи:

    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        var web = properties.Feature.Parent as SPWeb;
        var list = web.Lists["My list title"];
        var view = list.DefaultView;
        view.XslLink = @"../My/MyTransform.xsl";    view.Update();
    }

    Здесь я предполагаю, что фича имеет Scope=Web, XSLT-преобразование лежит в файле Layouts/My/MyTransform.xsl (путь указывается относительно папки Layouts/Xsl), а у списка заголовок “My list title” (хотя получать списки по заголовку нежелательно, лучше по Url или по Guid).

    Добавление элемента в библиотеку (Client Object Model)

    Делаем следующее:

    1. Открываем стандартное окошко добавления элемента в библиотеку рисунков
    2. В случае успешного добавления картинки, обновляем одно из полей выбранного элемента списка адресом этой картинки. Поле имеет тип "Hyperlink or picture".

    Код:

    var picturesLibraryGuid = 'put-your-guid-here';
    if (SP.ListOperation.Selection.getSelectedItems().length != 1) {
        SP.UI.Notify.addNotification('Select a single item, please!');
    }
    else {
        var itemId = SP.ListOperation.Selection.getSelectedItems()[0].id;
        var listGuid = SP.ListOperation.Selection.getSelectedList();
        var onError = function (sender, args) {
            SP.UI.Notify.addNotification('Error occured: ' + args.get_message());
        };
        var onItemUpdated = function () {
            SP.UI.Notify.addNotification('Item updated successfully!');
            SP.UI.ModalDialog.RefreshPage(1);
        };
        var options =
            {
                url: '/TeamSite/_layouts/Upload.aspx?List=' + picturesLibraryGuid,
                title: 'Upload picture',
                dialogReturnValueCallback:
                    function (dialogResult, returnValue) {
                        if (dialogResult == SP.UI.DialogResult.OK) {
                            var context = new SP.ClientContext.get_current();
                            var list = context.get_web().get_lists().getById(listGuid);
                            var item = list.getItemById(itemId);
                            item.set_item('Picture', returnValue.newFileUrl);
                            item.update();
                            context.executeQueryAsync(onItemUpdated, onError);
                        }
                    }
            };
        SP.UI.ModalDialog.showModalDialog(options);
     
    }

    Важные моменты в коде:
    • SP.ListOperation.Selection - здесь можно получить Guid текущего списка, а также ID и fsObjType выбранных элементов этого списка;
    • SP.UI.ModalDialog.RefreshPage - позволяет обновить содержимое текущей страницы, если диалог корректно отработал и предполагается, что данные на странице изменились. В качестве параметра передается обычно dialogResult.
    • SP.ClientContext - точка входа для SharePoint EcmaScript Client Object Model.
    • "_layouts/Upload.aspx" - Путь к форме загрузки файла в библиотеку документов. В качестве параметра, нужно передать Guid библиотеки, в которую будет происходить загрузка файла.
    • returnValue.newFileUrl - возвращаемое значение диалога Upload.aspx, содержит свойство newFileUrl, которое выставляется в Url загруженной картинки;

    Использование стандартных модальных диалогов Sharepoint

    В этой статье, касающихся родным модальных диалогов, я попытаюсь показать как использовать их.

    1. Color dialog


    Можно отобразить следующим кодом:

    SP.UI.ModalDialog.showModalDialog({
        url: '/_layouts/morecolors.aspx',
        args: currentColor, // сюда поместите значение для Old color
        dialogReturnValueCallback: function (dialogResult, returnValue) {
            if (dialogResult == SP.UI.DialogResult.OK)
                $get('myDiv').style.backgroundColor = returnValue;
        }
    });
    И в returnValue после закрытия диалога вы получите значение цвета в виде строки, например '#222222'

    2. Upload image to

    Чтобы дать пользователю возможность выбрать, в какую библиотеку загружать картинку, можно использовать диалог со следующим url'ом: 
    /_layouts/RTEUploadDialog.aspx?LCID=1033&Dialog=UploadImage&UseDivDialog=true
    Этот диалог выглядит следующим образом:


    Заголовок диалогу, естественно, можно менять произвольным образом. В раскрывающемся списке будут представлены все библиотеки узла. Вернет диалог строку, содержащую html-код, соответствующий тэгу img. 
       <img src="Shared Documents/test.img" alt="test.img" />
    Из него легко получить тот же самый Url с помощью простейшей jquery-конструкции: $(returnValue).attr('src').

    3. Upload document to

    Если заменить в предыдущем примере Dialog=UploadImage на Dialog=UploadDocument, получим аналогичную форму для загрузки документа произвольного типа. 


    Возвращаемое значение в этом случае будет иметь опять же вид строки с html-кодом, но на этот раз будет содержать ссылку на файл и img-тэг с картинкой, соответствующей типу файла.
    <a href="/Documents/test.doc" /><img class="ms-asset-icon ms-rtePosition-4" src="/layouts/images/iccss.gif" alt="test.doc">test.doc</a> 

    4. Text editor

    Чтобы проинициализировать значение этого диалога, нужно передать в качестве аргументов объект со свойством value, куда и поместить начальный текст:
    options.args = { value: "initial dialog text goes here" };
    Диалог вернет текст, введенный пользователем в окно ввода

    5. Attachments

    Форма /layouts/attachfile.aspx принимает параметры ListId (Guid списка) и ItemId (целочисленный ID элемента списка), и хотя ничего не возвращает, но понять, что вложение было успешно загружено, можно по dialogResult - а выяснять, что именно было загружено, придется уже через Client Object Model. Это, видимо, будет последний файл в соответствующем элементу списка каталоге вложений. Определить каталог вложений довольно просто:
    var listItemAttachmentsFolder = list.RootFolder.SubFolders["Attachments"].SubFolders[listItem.ID]; 
    Также, ничего не возвращает форма добавления нового элемента списка - тоже неприятно, и опять придется прибегать к помощи Client Object Model, хотя конечно очень хотелось бы получить ID записи в виде возвращаемого значения...

    понедельник, 19 ноября 2012 г.

    Магические всплывающие подсказки Javascript

    Все видели подсказки у Ribbon`a:

    Или даже такой
    Решение:
    Заключается в создании простенького класса, который отображает всплывающие подсказки аналогично тому, как это делает стандартный класс CUI.ToolTip. Создаем новый js файл запихиваем туда код:
    Type.registerNamespace("My.Namespace");
     
    My.Namespace.ToolTipManager = function () {
     
        var _divId = "my_tooltip";
        var _innerDivId = "my_tooltip_inner";
     
        this.attachToolTip = function (element, title, description) {
            $addHandler(element, 'mouseover', function (e) { showTooltip(element, createTitleAndDescriptionHtml(title, description)); });
            $addHandler(element, 'mouseout', hideDiv);
        }
     
        this.attachToolTipRaw = function (element, rawHtml) {
            $addHandler(element, 'mouseover', function (e) { showTooltip(element, rawHtml); });
            $addHandler(element, 'mouseout', hideDiv);
        }
     
        function createTitleAndDescriptionHtml(title, description) {
            return String.format(
                '<div class="ms-cui-tooltip-title">{0}</div><div class="ms-cui-tooltip-description">{1}</div>',
                title,
                description);
        }
     
        function showTooltip(element, rawHtml) {
            var tooltipDiv = $get(_divId);
            if (tooltipDiv == null)
                tooltipDiv = createTooltip();
     
            $get(_innerDivId).innerHTML = rawHtml;
     
            displayTooltipNextToElement(tooltipDiv, element);
        }
     
        function displayTooltipNextToElement(tooltipDiv, element) {
            tooltipDiv.style.display = '';
            var loc = Sys.UI.DomElement.getLocation(element);
            tooltipDiv.style.left = loc.x + 'px';
            tooltipDiv.style.top = loc.y + element.offsetHeight + 2 + 'px';
     
            if (tooltipDiv.curTimeout != null)
                clearTimeout(tooltipDiv.curTimeout);
        }
     
        function createTooltip() {
            var mainDiv = document.createElement('span')
            mainDiv.id = _divId;
            mainDiv.className = 'ms-cui-tooltip';
            mainDiv.style.width = 'auto';
            mainDiv.style.position = 'absolute';
     
            var bodyDiv = document.createElement('div');
            bodyDiv.className = 'ms-cui-tooltip-body';
            bodyDiv.style.width = 'auto';
     
            var innerDiv = document.createElement('div');
            innerDiv.id = _innerDivId;
            innerDiv.className = 'ms-cui-tooltip-glow';
            innerDiv.style.width = 'auto';
     
            bodyDiv.appendChild(innerDiv);
     
            mainDiv.appendChild(bodyDiv);
     
            document.body.appendChild(mainDiv);
     
            return mainDiv;
        }
     
        function hideDiv() {
            $get(_divId).style.display = 'none';
        }
    }

    Добавляем регистрацию класса (в web part`е).

    <asp:ScriptManagerProxy ID="asd" runat="server">
        <Scripts>
            <asp:ScriptReference Path="~/_layouts/TestProject/JS/Script.js?rev=1.0.0.0" />
        </Scripts>
    </asp:ScriptManagerProxy

    Или так (этот вариант подходит для обявления только методов)

    SharePoint:ScriptLink ID="ScriptLink1" language="javascript" name="/_layouts/TestProject/JS/Script.js?rev=1.0.0.0" 

    Совет!!!всегда указывайте версию JS, поскольку у Shapoint`а есть привычка кешировать скрипты, и поэтому при каждом последующем Deploy наращивайте ревизию.

    Вызов методов

    My.Namespace.ToolTipManager.attachToolTip($get('<%=Button1.ClientID%>'), 'Помощь', 'Это тестовая всплывающая подсказка!');
     
    Получаем


    А можно получить что - то типа такого

    Вызвав
    <script type="text/javascript">
        ExecuteOrDelayUntilScriptLoaded(Initialize, "sp.js");
     
        function Initialize() {
            var rawTemplate = '<div class="ms-cui-tooltip-footer">' +
                                '    <span class="ms-cui-img-16by16 ms-cui-img-cont-float">' +
                                '        <img style="vertical-align: top;" src="{0}" alt="" />' +
                                '    </span>' +
                                '    <div>' +
                                '        {1}' +
                                '    </div>' +
                                '</div>' +
                                '<div class="ms-cui-tooltip-clear"></div>' +
                                '<hr />' +
                                '<div class="ms-cui-tooltip-description">' +
                                '    {2}' +
                                '</div>' +
                                '<div class="ms-cui-tooltip-clear"></div>' +
                                '<hr />' +
                                '<div class="ms-cui-tooltip-footer">' +
                                '    <span class="ms-cui-img-16by16 ms-cui-img-cont-float">' +
                                '        <img style="vertical-align: top;" src="{3}" alt="" />' +
                                '    </span>' +
                                '    <div>' +
                                '        {4}' +
                                '    </div>' +
                                '</div>';
     
                    My.Namespace.ToolTipManager.attachToolTipRaw($get('<%=Button2.ClientID%>'),
                        String.format(rawTemplate,
                        '/teamsite/_layouts/images/TesttProject/pdf.png',
                        'Title',
                        'Hello world',
                        '/teamsite/_layouts/images/TesttProject/settings.png',
                        'Bottom bar - work'));
        }
    </script>

    пятница, 2 ноября 2012 г.

    Возникла неожиданная ошибка

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

    Типичный сценарий при открытии страницы:
     


    Или на русском:

    Ошибка
    Возникла неожиданная ошибка.
    Устранение неполадок в работе службы Microsoft SharePoint Foundation.
    Идентификатор взаимосвязи: a90f0e23-675e-4eee-ba8f-2cb3be4dcc3e

    Лекарство:

    Найти подробный текст ошибки можно в папке 14\LOGS.  В этой папке много файлов с логами, которые по умолчанию создаются каждые полчаса и в их названии указывается дата и время создания. Можно открыть их блокнотом и найти нужную строчку по CorrelationID, который указан на странице ошибки. Более удобно их открывать при помощи программы ULS Viewer. этой программой можно открыть как любой из старых файлов, так и текущий для просмотра логов в реальном времени. Удобно настраиваемые фильтры помогают отобрать нужные строки логов. Скачать ULS Viewer тут