Sunday 25 June 2017

Редактор имен файлов (MFC-FileEditor)

Очень часто возникает необходимость массово редактировать файлы. Заменить в именах файлов одни символы на другие.
 При этом, желательно, чтобы замена выполнялась по выбору:
  • по всему имени
  • только в начале имени
  • только в конце имени 
Total Commander прекрасно выполняет эту работу, но он не может заменить только первый или только последний символ в строке. Мои поиски в интернете соответствующей программы привели к Bulk Rename Utility. Однако по ряду причин она меня не устроила, поэтому и было принято решение написать самостоятельно собственный редактор имен файлов. 

Исходники программы опубликованы здесь

Редактор имен файлов (MFC-FileEditor) и выполняет  вышеуказанную и другие задачи.

Программа работает с двумя списками для хранения имен файлов и содержит панель управления. 


По команде загружает список имен и выбранного каталога.

Список хранит имена файлов выбранного каталога, кроме тех, которые предусмотрены панелью "Исключения"

Кроме этого, список имен файлов можно ограничить с помощью панели "Строки для удаления"

Способ редактирования имен файлов регулируется настройкой панели "Опции"

После проверки заданных параметров команду редактора можно выполнить.
Команда "Выполнить" создаст протокол действий и запишет его в .log файл, который можно обнаружить в корне папки инсталляции программы. 

Sunday 16 April 2017

Использование в проектах SharePoint библиотеки jqGrid

В SharePoint 2013(2016) для отображения элементов списка используется Client Side Rendering (CSR). Работа на стороне клиента в первую очередь гарантирована методами clienttemplates.js. Для нормальной работы сайтов на SharePoint этого вполне может быть достаточно, но очень часто возникают нестандартные ситуации, решение которых потребует подключить к проекту библиотеки сторонних разработчиков.
Сегодня я хотел бы показать небольшой пример работы с библиотекой  jqGrid. Это JavaScript-элемент с поддержкой Ajax, который предоставляет решения для показа и обработки табличных данных в Интернете.
Предположим, у нас имеется очень простой список (SPList) PMBirthdayBoys (ПМ-Именинники). В него, кроме обязательных столбцов, добавлен один  столбец - BirthdayBoy, тип SPUser. Данные этого списка нужно показать в отдельном окне JQuery Dialog
Следующий ниже JavaScript-метод легко решает эту задачу:

function GetDataPM(ListTitle) {
    var url = _spPageContextInfo.siteAbsoluteUrl +
                        "/_api/web/lists/getByTitle('" + ListTitle + "')/items?" +
"$select=Id,BirthdayBoy/Title,BirthdayBoy/Department,"+
BirthdayBoy/EMail,BirthdayBoy/JobTitle,"+
BirthdayBoy/Name&" +
                        "$expand=BirthdayBoy/Id,BirthdayBoy/Picture&" +
                        "$orderby=Id desc";
    var wWidth = $(window).width(), wHeight = $(window).height(), 
ContainetID = "InformersID", TableID = "Table_" + ContainetID;
    $('body').append($('<div />').attr('id', ContainetID).css('background-color''white')
.addClass('my-dialog')
                .append($('<div />').addClass('main-forms-dialog')));
    //-----------------------------
    SetDocData_pm(url, function (data) {
        var data_res = data.results;
        if (data_res.length > 0) {
            var grid = $("#" + TableID), dialog = $('#' + ContainetID);
            dialog.dialog({
                buttons: [{
                    text: "Закрыть", click: function () {
                        $(this).dialog('close');
                    }
                }
                ],
                height: wHeight,
                width: wWidth,
                closeText: "",
                modal: true,
                title: "Форма уведомлений",
                show: "slide",
                hide: "slide",
                resizable: true,
                create: function (event, ui) { },
                open: function () {                    
                    $('#' + ContainetID).remove('#' + TableID).children('div')
                                .append($('<div />').attr('id''jqGridPager')
                    .addClass('main-forms-col-dialog padding-10')).append($('<table />')
.attr('id', TableID));
                    var grid = $("#" + TableID);
                    //----------------------
                    grid.jqGrid({
                        regional: "ru",
                        data: data_res,
                        datatype: "local",
                        rowList: [],
                        pgbuttons: false,
                        pgtext: null,
                        viewrecords: true,
                        pager: "#jqGridPager",
                        colMenu: true,
                        height: wHeight * 0.7,
                        width: wWidth * 0.9,
                        rownumbers: true,
                        sortname: "invdate",
                        sortorder: "desc",
                        colModel: [
                         { name: "Id", width: 30, formatter: "integer", hidden: true
                         colmenu: false, key: true, index: "Id" },
                         { name: "BirthdayBoy.EMail", label: "EMail", colmenu: false },
                         { name: "BirthdayBoy.Title", label: "Именинник", colmenu: false },
                         { name: "BirthdayBoy.JobTitle"
                         label: "Должность", colmenu: false },
                         { name: "BirthdayBoy.Department"
                         label: "Место работы", colmenu: false },
                         { name: "BirthdayBoy.Name", label: "Логин", colmenu: false }
                        ]
                    })
                        .jqGrid("navGrid""#jqGridPager", { edit: false, add: false
                        del: false, search: true, refresh: false, view: false
                        position: "left", cloneToTop: true })
                        .jqGrid("gridResize")
                        .jqGrid("navButtonAdd""#jqGridPager", {
                            buttonicon: "ui-icon-mail-open",
                            title: "Поздравить именинника",
                            caption: "Поздравить",
                            position: "next",
                            onClickButton: function () {
                                var rowid = grid.jqGrid("getGridParam""selrow");
                                if (rowid) {
                                    var getCell_Id = grid.jqGrid('getCell', rowid, 'Id');
                                    //--что то делаем здесь
                                }
                                else {
           alert("Строка не выделена. Необходимо установить курсор мыши на строку таблицы.");
                                }
                            }
                        });
                },
                close: function () { $('body').find('#' + ContainetID).remove(); }
            });
        }
        else {
            alert("Список «Именинник» пустой...");
        } 
    }, true); 
Метод GetDataPM принимает имя списка. Далее выполняет запрос об указанном в списке пользователе, выясняет его электронный адрес, имя, логин и место работы (все эти данные хранятся в базе профилей пользователей SP). Инициализирует jqGrid таблицу, загружает туда полученные с сервера данные и сохраняет все это в форме JQuery Dialog
Вот и все....😄
В моем случае все прекрасно работает. Публикую эту информацию в надежде, что она может быть кому нибудь пригодится. А может быть, кто нибудь укажет мне на явные ошибки, что очень приветствуется😂.





Saturday 15 April 2017

Решение для фермы SharePoint. Учет сотрудников

Недавно меня попросили создать решение для демонстрации технологий SharePoint:
  • CAML
  • TimerJob
  • WebPart
  • ControlTemplates
  • WCF 
Решение должно: 
  1. Разворачивать шаблон списка SharePoint для хранения информацию о сотрудниках и их днях рождения;
  2. Создавать экземпляр этого списка с 5 тестовыми записями; 
  3. Содержать страницу с настройками решения;
  4. Реализовать механизм рассылки уведомлений о приближающихся днях рождения менеджеру персонала на указанный в настройках e-mail за указанное в настройках количество дней;
  5. Содержать webpart, который отображает ближайшие дни рождения из этого списка (у webpart должны быть настройки за сколько дней до ДР уведомление должно появляться на странице);
  6. Быть разработано для сервера SharePoint 2013 и предназначено для учета сотрудников; предприятия. Акцент в решении сделан на дни рождения сотрудников.
Я подготовил в соответствии с задачей необходимый проект.

Состав проекта:

1. Шаблоны списков:
  • MPListEmployees. Для хранения информации о сотрудниках 
  • MPEmployeesResursec. Для хранения служебной информации
  • PMBirthdayBoys. Для краткосрочного хранения информации о днях рождения 
2. Две веб части:
  • VPMEmployees.webpart. Для просмотра сведений об именинниках
  • VPMAdmin.webpart. Для установки списков решения на сайтах фермы SharePoint


Описание проекта:
Количество списков в решение и их поля регламентированы необходимостью полностью выполнить поставленные задачи с наименьшими затратами и с наибольшей точностью.
Некоторые поля списков не используются, так как сделаны с прицелом на будущее развитие.
В момент инсталляции решения:
 1. Его ресурсы добавляются в папки фермы:
  • CONTROLTEMPLATES
  • IMAGES
  • FEATURES
  • LAYOUTS
2. В список MPListEmployees загружаются сведения о 5-и учетных записях.
3. Администратору фермы направляются советующие уведомления. 
4. В решении использованы технологии:
  • CAML
  • TimerJob
  • WebPart
  • ControlTemplates
  • WCF 
Моя реализация поставленной задачи потребовала создать в проекте два решения:
  1. Первое - PMEmployeesTimerJob выполняет задания таймера.
  2. PMEmployees - делает все остальное, в т.ч. содержит ресурсы.  
Проект - исходные файлы

Метод SetUrlKeyValue и его JQuery аналог

Недавно, работая над очередным решением для фермы SharePoint 2013, я использовал метод JavaScript - SetUrlKeyValue для редактирования параметра в строке запроса. 

SetUrlKeyValue (KEYNAME, KeyValue, bEncode, URL)

Устанавливает значение ключа в URL. Третий параметр указывает - необходимо ли значение кодировать.

Определение метода я нашел в файле INPLVIEW.debug.js

function SetUrlKeyValue(keyName, keyValue, bEncode, url) {
    if (url == null)
        url = window.location.href + "";
    var val = keyValue;
    var uri = new URI(url, {
        disableEncodingDecodingForLegacyCode: true
    });
 
    url = uri.getQuery();
    if (bEncode)
        val = escapeProperly(val);
    if (url.indexOf(keyName + "=") < 0) {
        if (url.length > 1)
            url += "&";
        else if (url.length == 0)
            url += "?";
        url += keyName + "=" + val;
    }
    else {
        var re = new RegExp(keyName + "=[^&]*");
 
        url = url.replace(re, keyName + "=" + val);
    }
    uri.setQuery(url);
    return uri.getString();
}

И все было прекрасно, метод великолепно выполнял свое назначение, и я был доволен. 
Тем не менее, изначально, я не учел очень важную особенность работы метода SetUrlKeyValue - он выполняется в среде, запущенной от имени пользователей, обладающих, как минимум, правами для совместной работы с текущим контентом.
Меня это не устраивало, так как метод должен выполняться и для пользователей с правами только для чтения текущей страницы. И таких пользователей, как правило, должно быть большинство. Не давать же в конце концов им всем права на совместное пользование сайтом SharePoint☺.

Вот фрагмент моей функции, который изначально мною использовался:
$("a[title='zamena']").each(function () {
        var url = $(this).attr("href");
        var _BZnaniyId_ = url.indexOf('BZnaniyId=');
        if (_BZnaniyId_ > 0) {
            var BZnaniyId = parseInt(GetUrlKeyValue("BZnaniyId"));
            if (BZnaniyId > 1) {
                try{
                    var url1 = SP.Utilities.UrlBuilder.removeQueryString(url, "BZnaniyId");
                    if (typeof url1 != "undefined") {
                        var url2 = SetUrlKeyValue("BZnaniyId", BZnaniyId, false, url1);
                        if (typeof url2 != "undefined") {
                            $(this).attr("href", url2);
                        }
                    }
                }
                catch (e)
                {
                    console.log('Ошибка: ' + e);
                }
            }
        } 
    });
Присваивание значения переменной  url2 вызывало исключение. После некоторых экспериментов я решил поменять способ инициализации переменной. И это стало выглядеть следующим образом:
$("a[title='zamena']").each(function () {
        var url = $(this).attr("href");
        var _BZnaniyId_ = url.indexOf('BZnaniyId=');
        if (_BZnaniyId_ > 0) {
            var BZnaniyId = parseInt(GetUrlKeyValue("BZnaniyId"));
            if (BZnaniyId > 1) {
                try{
                    var url1 = SP.Utilities.UrlBuilder.removeQueryString(url, "BZnaniyId");
                    if (typeof url1 != "undefined") {
                        var url2 = updateQueryStringParameter("BZnaniyId", BZnaniyId, url1);
                        if (typeof url2 != "undefined") {
                            $(this).attr("href", url2);
                        }
                    }
                }
                catch (e)
                {
                    console.log('Ошибка: ' + e);
                }
            }
        }
    });

Для инициализации переменной url2 я использовал другой метод чтения и редактирования строки запроса.

Реализация этого метода выглядит следующим образом:
function updateQueryStringParameter(key, value, uri) {
    var re = new RegExp("([?&])" + key + "=.*?(&|$)""i");
    var separator = uri.indexOf('?') !== -1 ? "&" : "?";
    if (uri.match(re)) {
        return uri.replace(re, '$1' + key + "=" + value + '$2');
    }
    else {
        return uri + separator + key + "=" + value;
    }
}

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

Wednesday 21 December 2016

Управление большими списками и библиотеками в SharePoint

Управление большими списками и библиотеками в SharePoint очень актуальная и востребованная тема, как для администраторов, так и для программистов. 
И, если для администратор SharePoint в сети опубликовано достаточно качественного (ну, качество материала суждение субъективное😆, и я об этом немного после порассуждаю) справочного материала, например этот, то программистам немного сложнее изыскать соответствующий материал. 
Более того,  подходы к работе с большими данными в SharePoint Online и SharePoint 2013 (2016) разный. Об этом, кстати, совершенно справедливо сказано в этой статье.
Сегодня я планирую описать свой опыт при работе с большими данными в SharePoint 2013 с использованием Business Connectivity Services (BCS) компонентов на стороне сервера.

Да, именно в этом случае проблемы работы с большими объемами списков или библиотек наиболее актуальны, так как приходиться самостоятельно формировать запрос к источнику данных. 
Известно, что в SharePoint 2013(2016) и SharePoint Online существуют пороговое ограничение получения списка или библиотеки в 5000 элементов, которое можно регулировать. В SharePoint Online возможность регулирования порогового ограничения отсутствует. В случае, если представление списка или библиотеки возвращает число элементов большее порогового ограничения, то будет сгенерировано  исключение.
Вот теперь подробнее о том, как следует построить решение (SPSolution) SharePoint, которое будет запрашивать большое число элементов и не создавать исключений превышения порогового значения, установленного администратором портала☺:
Мы не будем обсуждать варианты увеличения порогового знания на глобальном уровне в центре администрирования. Как известно, по умолчания это 5000 элементов.


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

Первый вариант. Свойство QueryThrottleMode класса SPQuery

Перечислитель QueryThrottleMode содержит 3 варианта использования:
1. Default
2. Override
3. Strict
Для нас интересен параметр Override. Как сказано в документации - "Значение перечисления для переопределения: 1. Если пользователь является локальным администратором на сервере без ограничения полосы пропускания будет применяться в запросе. Если политика безопасности приложения web предоставляет разрешения чтение или полный доступ пользователя предел регулирования для аудиторов и администраторов будет применяться к числа элементов, включенных в запрос и регулирование не будет применяться к число подстановки, пользователей или групп и полей состояния рабочего процесса. В противном случае параметр [Microsoft.SharePoint.SPQueryThrottleOption.SPQueryThrottleOption.Default] применяется к запросу."
На практике перечислитель QueryThrottleMode используется следующим образом (снятие ограничения на уровне запросов):
//создаем ссылку на список
SPList list = web.Lists["ListName"];
//создаем запрос
SPQuery query = new SPQuery();
query.Query = @"<View Scope='RecursiveAll'>
               <ViewFields>
                  <FieldRef Name='ID' />
                  <FieldRef Name='Title' />                                                                 
               </ViewFields> 
               <Query>
                  <Where>
                     <Eq>
                        <FieldRef Name='Title' /><Value Type='Text'>текст для фильтра</Value>
                     </Eq>
                  </Where>  
                </Query>                            
                </View>";
//меняем перечислить запроса
query.QueryThrottleMode = SPQueryThrottleOption.Override;
//создаем ссылку на массив строк 
SPListItemCollection results = list.GetItems(query);
//далее используем переменную results по назначению, например так
foreach (SPListItem item in results)
{
//здесь ваш код....👧  
}
Вот и все. Следует добавить, что данный код будет работать, как следует только тогда, когда запрос выполняет пользователь с привилегированными правами. Это администраторы ферм или те, кому предоставлен полный доступ к запрашиваемым ресурсам. Не очень удобная ситуация, да 😆, тем более, эти пользователи и так имеют привилегии для просмотра ресурсов, предоставленные им методами регулирования ресурсов в центре администрирования портала. И поэтому, указанные выше метод, мягко говоря непрактичен 😄😄😄😄😄😄. 
Конечно, программисты SharePoint, могут обернуть код в конструкцию SPSecurity.RunWithElevatedPrivileges и тогда все запросы будут осуществляться от имени администратора портала. Но и это, не очень умное решения, хотя бы с точки зрения производительности и безопасности запросов😂.  Но не все так печально, так как у нас имеется вариант № 2 😆

Второй вариант ContentIterator:

Класс ContentIterator предоставляет методы для выполнения запросов элементов списка, списков и веб-узлов для регулирования объема передаваемых данных (т.е., чтобы избежать возникновения SPQueryThrottledException).
В справке класса приведен небольшой пример его использования. Поэтому я не буду ссылаться на код из своей практики. Они, практически идентичны. Замечу только, что путем опытных экспериментов я выяснил, что для меня класс не совсем приемлем, ну, хотя бы потому, что у меня с помощью этого класса не получилось "практично" запросить данные списка по нескольким столбцам. "Выдача" осуществляется только по одному столбцу. И этот столбец точно не может иметь тип Note. Поэтому я представлю третий вариант, который на мой взгляд, самый "практичный", по крайней мере. для меня😄😄😄😄😄😃😜

Третий вариант BatchQueryExector:

Итак, меня не устроил первый вариант и второй вариант, следовательно нужно мастерить нечто среднее между ними, позаимствовав у них все самое лучшее 😃. Я обратился за помощью к google, который привел меня к статье автор Charles Chen. Там описан класс, который и выполняет всю работу, так как это хотел я. Если коротко - класс запрашивает строки списков постранично. В общем ничего нового в подходах в работе с SQL данными, только подход перенесен на посредника😅. Я практически ничего не менял в классе, кроме метода GetItems. В моем варианте он выглядит так:
            /// <summary>
            ///     Извлекает элементы в списке в пакетах, основанных на <c>RowLimit</c> и 
            ///     вызывает обработчик для каждого элемента.
            /// </summary>
            /// <param name="handler">Метод, который вызывается для каждого элемента.</param>
            public void GetItems(Action<SPListItem> handler, bool Single = false)
            {
                string pagingToken = string.Empty;
 
                while (true)
                {
                    _query.ListItemCollectionPosition = new SPListItemCollectionPosition(pagingToken);
 
                    SPListItemCollection results = _list.GetItems(_query);
 
                    foreach (SPListItem item in results)
                    {
                        handler(item);
                    }
 
                    if (results.ListItemCollectionPosition == null || Single)
                    {
                        break// EXIT; no more pages.
                    }
                    pagingToken = results.ListItemCollectionPosition.PagingInfo;
                } 
            } 
Я думаю, что тот, кто сравнит исходник с моим вариантом, поймет "в чем новшество" и зачем?😃😏
Здесь я приведу пример использования класса. Пример из реального решения без ретуши и изменений😃:

Первый, где нужно получить только одну строку

            using ( SPSite siteNew = new SPSite ( this.SiteName ) )
            {
                using ( SPWeb webNew = siteNew.OpenWeb ( ) )
                {
                    try
                    {
                        string title = "";
                        SPList list = webNew.Lists[MConst.MFC_WikiChita_BZnaniy];//находим список                                               
                        SPQuery query = QueryBZnaniy.GetQuery(Single:true, ID: id);                                                
                        BatchQueryExector.WithQuery(query).OverList(list).GetItems(item => 
                        {
                            if (item != null)
                            {
                                title = item.Title;
                            }
                        },true);
                        return title;
                    }
                    catch (Exception e)
                    {
                        perror.Visible = true;
                        perror.CssClass = "padding-10 ui-state-error";
                        lerror.Text = e.ToString();
                        SendEmail.SendErr(webNew, e.ToString());
                        return "null";
                    }
                    finally
                    {
                        if (webNew != null)
                            webNew.Dispose();
                    }
                }
            }

Второй, где надо получить массив строк:

            using ( SPSite siteNew = new SPSite ( this.SiteName ) )
            {
                using ( SPWeb webNew = siteNew.OpenWeb ( ) )
                {
                    try
                    {
                        SPList list = webNew.Lists[MConst.MFC_WikiChita_BZnaniy];//находим список                                                  
                        var query = QueryBZnaniy.GetQuery();
                        List<BZnaniy> it = new List<BZnaniy> ( );
                        BatchQueryExector.WithQuery(query).OverList(list).GetItems(item =>
                        {
                            if (item != null)
                            {
                                SPFieldUrlValue fieldValue = 
new SPFieldUrlValue(item["URL"].ToString());
                                SPFieldUrlValue URL_BZ_Site = 
new SPFieldUrlValue(item["URL_BZ_Site"] != null ? item["URL_BZ_Site"].ToString() : null);
                                string title = 
item["Title"] != null && !string.IsNullOrEmpty(item["Title"].ToString()) ? 
item["Title"].ToString().ToUpper() : "[нет данных]";                                
                                it.Add(new BZnaniy
                                {
                                    id = item.ID,
                                    Title = title,
                                    UrlImage = fieldValue != null ? fieldValue.Url : "#",
                                    UrlSite = 
URL_BZ_Site != null && !string.IsNullOrEmpty(URL_BZ_Site.Url) ? 
URL_BZ_Site.Url : this.Page.Request.Url.AbsolutePath + "?BZnaniyId=" + item.ID;
                                });      
                            }
                        });                                                
                        return it;
                    }
                    catch ( Exception e )
                    {
                        perror.Visible = true;
                        perror.CssClass = "padding-10 ui-state-error";
                        lerror.Text = e.ToString ( );
                        SendEmail.SendErr(webNew, e.ToString());
                        return new List<BZnaniy> ( );
                    }
                    finally
                    {
                        if (webNew != null)
                            webNew.Dispose();
                    }
                }
            }
В реальном решении третий вариант работы с большими данными прекрасно себя зарекомендовал. Единственное замечание - пожалуйста, не забывайте индексировать столбцы списков или библиотек, которые участвуют в фильтрах запросов. Ну, это стандартные рекомендации😃. Они актуальные и разумные для всех случаев😃😃😃😃😃😃  

Tuesday 15 November 2016

Мастер назначения полномочий исполняющим обязанности

На порталах, сделанных в SharePoint, кроме прочего, очень гибкая и надежная система авторизации и профилей пользователей. Данная система удобна для больших и средних (и даже маленьких 😶) организаций, у которых допуск к ресурсам регламентируются должностными обязанностями сотрудников.
Часто происходят случаи, когда должностные лица, у которых уровни доступа к ресурсам разные, замещают друг друга. И тогда администратору портала следует выполнить вручную определенные действия, чтобы замещающие обязанности лица получили доступ к ресурсам на время выполнения таких обязанностей.
Для перераспределения обязанностей между сотрудниками организации мною и был создан мастер "Мастер передачи прав и обязанностей".
Это решение для фермы, которое выполняется в следующей последовательности:
1. Если необходимо править права на портале:
Пользователь создает в мастере задачу на передачу прав. При этом указывает: роль, которую следует править и период передачи прав.
2. Если необходимо передать обязанности по задачам:
Здесь пользователю предлагается выбрать имена задач, по которым он является ответственным и которые на время работы с мастером еще не окончены. Или "приказать" "мастеру" искать самостоятельно все его незакрытые задачи на портале.
3. Если необходимо передать управление несогласованными документами:
Здесь, по аналогии с задачами. Только вместо задач "мастер" будет работать с документами.
После того, как пользователь создает соответствующую задачу в "мастере", в работу вступает "робот". Он ежедневно читает список задач "мастера", и если находит для себя работу, то выполняет ее. Например, он может либо отредактировать указанные ему роли пользователей портала, или переписать задачи или документы на указанного ему исполнителя. 
Все очень просто. "Робот" в автоматическом режиме выполняет работу администратора портала, который в свою очередь "курит бамбук" 😶 или еще что-либо 😶😶😶

Некоторые скриншоты "мастера":









Если вас заинтересовал материал, то я обязуюсь ответить на ваши вопросы и (или) оказать практическую помощь при создании аналогичного решения для вашей фермы SharePoint.
😶😶😶😶😶😶😶😶😶