Расширенная функциональность
22
Авг
1

CRM 2016 или найди 10 отличий… Разработка, эпизод I

Специальные операции

MS все больше унифицирует систему, избавляясь от не совсем очевидных артефактов прошлого.

Например, в предыдущих версиях CRM, чтобы изменить Состояние записи необходимо было выполнить запрос SetStateRequest. А для того чтобы сменить Ответственного необходимо было выполнить AssignRequest. Сейчас все эти операции можно выполнить в рамках UpdateRequest.

Полный список сообщений, которые перетекли в UpdateRequest следующий:

  • AssignRequest
  • SetStateRequest
  • SetParentSystemUserRequest
  • SetParentTeamRequest
  • SetParentBusinessUnitRequest
  • SetBusinessEquipmentRequest
  • SetBusinessSystemUserRequest

Важно: в дальнейших версиях CRM MS вовсе удалит эти сообщения, поэтому лучше сейчас начать их заменять.

Также учтите, Вы не сможете теперь отлавливать отдельные события — теперь Вам нужно будет отлавливать Update и «вручную» проверять необходимые изменения.

Пример смены владельца и Состояния записи:

Entity contact = new Entity();
contact.LogicalName = "contact";
contact.Id = new Guid("A925C42D-CFE4-E411-80E8-C4346BAD5414");
contact["fax"] = "412";
// Задаем нового Ответственного
contact["ownerid"] = new EntityReference("systemuser", new Guid("FB8C44BF-B0E9-E411-80ED-C4346BAD5414"));
contact["statecode"] = new OptionSetValue(1);
// Меняем статус
contact["statuscode"] = new OptionSetValue(2);
// Отправляем запрос на обновление
service.Update(contact);

Хотя все эти ранее отдельные сообщения объеденены одним Update’ом, внутри системы это вызовет запрос Update на каждое из этих сообщений. Т.е. пример выше приведет к трем событиям Update:

  • 1-й раз для обновления поля Факс;
  • 2-й раз для Владельца записи;
  • 3-й раз для смены Состояния.

Глубина каждого события будет равна единице.

Примечание:

  • До такого как специальные сообщения будут удалены их прямое использование будет работать через метод Update. Т.е., например, вызов AssignRequest будет переводится внутри системы в UpdateRequest. Хотя само сообщение Assign будет присутствовать в конвеере.
  • К сожалению, CreateRequest пока не затронуло это улучшение. И хотя в нем можно задать владельца, но задавать Состояние необходимо по прежнему отдельным запросом.
  • Когда Вы изменяете Ответственного или Состояние записи через пользовательский интерфейс CRM, при этом все еще будут использоваться сообщения SetState и Assign.
  • БП, зарегистрированные на Назначение и смену Состояния, будут работать, как и раньше.

Интеграция

CRM SDK получил три важных улучшения, которые сильно упростят интеграцию CRM с другими системами.

Альтернативные ключи

Альтернативные ключи — новая функция, которая главным образом предназначена для упрощения интеграции и синхронизаций между CRM и сторонними системами

Если по-простому, Альтернативные ключи это второй GUID для записи CRM, посредством которого Вы сможете к ней обращаться. Обычно в качестве такого альте нативного ключа выступает идентификатор записи из сторонней системы.

Ранее, при интеграции новых записей в CRM приходилось проводить с ними ряд манипуляций. Например:

  • Искать по стороннему идентификатору записи в CRM, чтобы предотвратить создание дубликатов.
  • При обновлении записей предварительно искать и возвращать записи из CRM, чтобы получить их GUID по которому можно обновить запись.
  • Искать записи в CRM чтобы подставить их в лукап.
  • И т.д.

Но с Альтернативными ключами все эти операции (довольно дорогие по времени) останутся в прошлом 🙂 После того, как Альтернативный Ключ активен и по нему создан индекс (делается это автоматически), Вы готовы использовать его в своем коде.

Создание

Чтобы создать новый Альтернативный ключ:

  • Создайте новое поле одного из трех типов (или можете использовать уже существующие поля):
    • Одна строка текста.
    • Целое число.
    • Десятичное число.
  • Перейдите в раздел Ключи в настройках объекта и нажмите Новый.
    • Введите отображаемое и системное имя ключа.
    • Выберите поле, которое Вы создали, и нажмите Добавть. Заметьте, что Вы можете создать составной ключ, выбрав для него несколько полей.
  • Система попытается создать на этот ключ отельный индекс в БД, чтобы гарантировать быстрое выполнение запросов по кллючу. Создавться индекс будет с посощью Системного задания, которое запустится сразу после создания ключа. В зависимости от количетва данных, которое имеется в БД – это может занять продолжительное время. Пока процесс не завшершится Статус у ключа будет Ожидание (Pending) или В процессе (Progress).

П.С. Если индекс не создастся, то ключ не будет активирован. Причины могут быть разные – например, в текущих данных уже есть не уникальные значения и т.д. Подробное описание ошибки можно прочитать в Системном задании.


Примечания:

  • Для одного объекта можно определить до 5 различных Альтернативных ключей.
  • Для Альтернативного ключа можно использовать как стандартные так и кастомные поля.
  • Поскольку ключ не может иметь повторяющихся значений, то при попытке создать или изменить Альтернативный ключ на тот, который уже есть в системе, Вы получите ошибку.

Примечание: по это причине Альтернативные ключи можно использовать как некий аналог правил обнаружения дубликатов. Например, использовав в качестве Альтернативного ключа стандартное поле Название объекта Организация.

Использование

Классы Entity и EntityReference обзавелись новыми конструкторами, в которых используются альтернативные ключи:

public Entity(string logicalName, Guid id) {…}
public Entity(string logicalName, string keyName, object keyValue) {…}
public Entity(string logicalName, KeyAttributeCollection keyAttributes) {…}
public EntityReference(string logicalName, Guid id) {…}
public EntityReference(string logicalName, string keyName, object keyValue) {…}
public EntityReference(string logicalName, KeyAttributeCollection keyAttributeCollection) {…}

Следующие фрагменты кода показывают, как использовать Альтернативные ключи в операциях CRUD:

Создание записи с Альтернативным ключом

Entity account = new Entity("account", "accountnumber", "ERP12345");
account["name"] = "Alternate Key Test";
service.Create(account);

Обновление записи по Альтернативному ключу

Entity accountUpd = new Entity("account", "accountnumber", "ERP12345");
accountUpd["websiteurl"] = "www.test.com";
service.Update(accountUpd);

Ссылка на запись (в лукапе) по Альтернативному ключу

Entity contact = new Entity("contact); 
contact["parentcustomerid"] = new EntityReference("account", " accountnumber ", "123");
crmService.Create(contact);

Следующие операции не могут быть выполнены посредством Альтернативных ключей:

  • Удаление.
  • Возвращение (Retrieve).

Upsert

Термин Upsert пришел к нам из баз данных и происходит от объединения слов Update и Insert, т.е. обновить и вставить.

UpsertRequest это новое сообщение в CRM SDK, которое обновляет запись если она существует на момент выполнения запроса или создает новую если таковая отсутствует. При этом проверка происходит автоматически на стороне сервера и Вам не нужно выполнять никаких дополнительных запросов для того, чтобы проверить существование записи.

Проверка существования записи производится только по Альтернативным ключам.

Если раньше нужно было делать так:

QueryByAttribute query = new QueryByAttribute("contact");
query.AddAttributeValue("new_key", "1");
EntityCollection results = service.RetrieveMultiple(query);

if (results.Entities.Count == 1)
{
   Entity contactUpdate = new Entity("contact");
contactUpdate["firstname"] = "1-name";
   contactUpdate.Id = results.Entities[0].Id;
   service.Update(contactoUpdate);
}
else
{
   Entity contactCreate = new Entity("contact");
contactCreatet["firstname"] = "1-name";
   service.Create(contactCreate);
}

То теперь достаточно так:

Entity contact = new Entity("contact");
contact.KeyAttributes.Add("new_key", "1");
contact["firstname"] = "1-name";
UpsertRequest req = new UpsertRequest();
req.Target = contact;
UpsertResponse res = (UpsertResponse)service.Execute(req);

Когда Вы выполните этот код, CRM сначала проверит, есть ли запись Контакт, у которой есть Альтернативный ключ new_key со значением единица. Если таковая будет найдена, то поле firstname будет обновлено. Иначе будет создана новая запись с соответствующими значениями в полях new_key и firstname.

Ответ UpsertResponse содержит параметр RecordCreated (true/false), с помощью которого мы можем определить, была ли запись создана или обновлена.

З.Ы. С т.з. конвеера выполнения запросов не существует никакого отдельного сообщения Upsert. Для конвеера запрос Upsert это, в зависимости от ситуации, или Create или Update. Соответственно на Upsert нельзя зарегистрировать плагин. А плагины и БП будут срабатывать на отдельные события создания и обновления.

Отслеживание изменений

Часто в проектах интеграции необходимо вернуть только те записи, которые были созданы или изменены с момента последней интеграции. Раньше для этого нужно было запрашивать тот же объем данных и производить поэлементно сравнение, либо же запрашивать только данные с более поздней датой modifiedon и/или createdon. Если объем данных был вели, то все это не очень хорошо сказывалось на производительности процесса.

Отслеживание изменений объекта это новый функционал, который позволяет Вам определить, были ли изменены данные, которые Вы ранее получили. Имея такую информацию, Вы можете повторно запросить только их.

Чтобы отследить изменения данных определенного объекта, Вы должны сначала включить опцию Отслеживание изменений в настройках объекта (у большинства стандартных объектов она включена и неотключаема).


Каждый раз, когда Вы используете сообщение RetrieveEntityChangesRequest, ответное сообщение будет содержать маркер отслеживания. Когда Вы снова выполните запрос и включите внего прошлый маркер отслеживания – будут возвращены только измененные или добавленные записи объекта. Пример:

using (var service = new OrganizationService(crmConnection))
{
    var request = new RetrieveEntityChangesRequest();

    request.EntityName = "account";
    request.Columns = new ColumnSet("accountnumber", "name", "creditlimit");
    request.PageInfo = new PagingInfo() { Count = 5000, PageNumber = 1 };
    request.DataVersion = changeToken; // Установка токена возвращенного в прошлый раз (установите в NULL или удалите строку для первоночального запроса)

    var response = (RetrieveEntityChangesResponse)service.Execute(request);

    // TODO: Process all the changed records (see the code snippet below)

    // Сохранение токена для следующего запроса
    changeToken = response.EntityChanges.DataToken;
    Console.WriteLine(changeToken);
}

После получения изменений Вы получите два типа результатов:

  • Новые или обновленные записи;
  • Записи, которые были удалены начиная с последнего выполнения запроса.
foreach (var change in response.EntityChanges.Changes)
{
    if (change.Type == ChangeType.NewOrUpdated)
    {
        var changedItem = (NewOrUpdatedItem)change;
        Entity newOrChangedEntity = changedItem.NewOrUpdatedEntity;
        // TODO: обработка новых или обновленных записей
        if (newOrChangedEntity != null)
        {
            string firstName = newOrChangedEntity.GetAttributeValue<string>("name");
            string dataToken = response.EntityChanges.DataToken;
            string changedType = changedItem.Type.ToString();

            Console.WriteLine("DataVersion={0} ChangeType={1} FirstName={2}", dataToken, changedType, firstName);
        }
    }
    else if (change.Type == ChangeType.RemoveOrDeleted)
    {
        var deleteditem = (RemovedOrDeletedItem)change;
        EntityReference deletedEntityReference = deleteditem.RemovedItem;
        // TODO: обработка удаленных записей
    }
}

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

Также обратите внимание, что возвращенный NewOrChangedEntity – экземпляр класса Entity, а RemovedItem является экземпляром класса EntityReference.

Примечания:

  • Вследствие того, что Отслеживание изменений работает на уровне объекта, Вы должны отслеживать маркер изменения для каждого объекта индивидуально. Нет никакого глобального маркера отслеживания изменений.
  • Есть одна уникальная ситуация, которую Вы должны учитывать при написании кода. Предположим, после последнего получения данных была создана новая запись и она же была удалено перед новым получением данных. Ткая запись придет в наборе RemovedItem, хотя никакой предыдущей информации об той записи у Вас нет.
  • При запросе полей лукап, поле Name не вернется.
  • У учетной записи пользователя под которой выполняется запрос должно быть чтение уровня организации для нужного объекта, чтобы выполнить RetrieveEntityChangesRequest.
  • Если запрос выполняется без временного маркера, то сервер обработает его как запрос с минимальной версией маркера и вернет все записи как новые. Удаленные объекты не будут возвращены.
  • Измененные данные будут возвращены, только если последний маркер будет в пределах 90 дней (значение по умолчанию). Если он будет превышать 90 дней, то система вернет все записи.

JavaScript

Subgrid

Через JS теперь легально доступна объектная модель вложенного Представления на форме.

Событие OnLoad

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

Т.к. нет никакого пользовательского интерфейса чтобы связать событие OnLoad сабгрида с функцией JS, то делать придется это кодом:

  • GridControl.addOnLoad
  • GridControl.removeOnLoad

Пример:

var onLoadSubGrid = function () {
    alert("Load subGrid");
};

function onLoadForm() {
    var contactsSubgrid = Xrm.Page.getControl("Contacts");
    contactsSubgrid.addOnLoad(onLoadSubGrid);
}

getViewSelector

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

А с использованием функции объекта getViewSelector Вы можете управлять этим списком:

  • getCurrentView – возвращает ссылку на текущее выбранное представление
  • isVisible – позволяет определять, видим ли селектор представлений
  • setCurrentView – устанавливает текущее представление. Эта функция не будет работать если селектор невидим.

Пример установки текущего кастомного Представления:

var contactViewtobeSet = "";
contactViewtobeSet = {
    entityType: 1039, // SavedQuery
    id: "{ 00000000-0000-0000-00AA-000010001004}",
    name: "Active Contacts"
}

Xrm.Page.getControl("Contacts").getViewSelector().setCurrentView(contactViewtobeSet);

Примечание: если при выполнение этого кода у сабгрида не будет выведен селектор, то он выдаст ошибку.

getGrid

  • getRows
  • getSelectedRows
  • getTotalRecordCount

Наверное самое полезное улучшение – объект getGrid позволяет получить доступ к строкам вложенного Представления.

Примеры…

Помещаем все строки сабгрида в массив и подсчитываем сумму одного столбца:

// Помещаем все строки сабгрида в массив
var allRows = Xrm.Page.getControl("Child").getGrid().getRows();
var allGridRowData = [];
allRows.forEach(function (row, i) {
    allGridRowData.push(row.getData());
});

var sum = 0;
// Читаем все строки массива и суммируем один столбец
for (var i = 0; i < allGridRowData.length; i++)
{
    sum += parseInt(allGridRowData[i].getEntity().getAttributes().getByName("dynamics_integer").getValue());
}
Xrm.Utility.alertDialog("Sum of integers is: " + sum.toString());

Получаем ссылку на отдельную запись:

var recEntityReference = Xrm.Page.getControl("Contacts").getGrid().getRows().get(0).getData().getEntity().getEntityReference();

Получаем общее кол-во записей:

var rowCount = Xrm.Page.getControl("Contacts").getGrid().getTotalRecordCount();

getEntity

  • getEntityReference
  • getEntityName
  • getId
  • getPrimaryAttributeValue

Этот набор функций объекта getEntity полволяет получить GUID или ссылку на запись какой-либо строки.

И, в качестве бонуса, несколько незадокументиованных методов:

Get

Описание: возвращает сторку по определенному номеру в списке.
Объект: GridRow Collection.

Xrm.Page
.getControl("GridControlName")
.getGrid()
.getRows()
.get(0)

getByFilter

Описание: возвращает только строки, отфильтрованные по определенному значению (в данном примере с fullname = Max Power).
Объект: GridRow Collection

Xrm.Page
.getControl("GridControlName")
.getGrid()
.getRows()
.getByFilter(function (r) {
    return r.getData()
    .getEntity()
    .getAttributes()
    .getByName('fullname')
    .getValue() === 'Max Power';
})

getFirst

Описание: возвращает только первую строку массива строк отфильтрованного по определенному значению.
Объект: GridRow Collection.

Xrm.Page
.getControl("GridControlName")
.getGrid(
.getRows()
.getByFilter(function(r){
    return r.getData()
    .getEntity()
    .getAttributes()
    .getByName('fullname')
    .getValue() === 'Max Power';
})

getValue

Описание: Возвращает значение определенного атрибута.
Объект: атрибут.

Xrm.Page.getControl("GridControlName")
.getGrid()
.getRows()
.get(0)
.getData()
.getEntity()
.getAttributes()
.getByName("fullname")
.getValue()

openAssociatedGrid

Описание: открвыает вложеное Представление как связанное.
Объект: Grid.
[/javascript]
Xrm.Page.getControl(«GridControlName»)
.openAssociatedGrid()
[/javascript]

Формы

openQuickCreate

Используя новую функцию Xrm.Utility.openQuickCreate Вы можете открыть форму быстрого создания. Синтаксис:

Xrm.Utility.openQuickCreate(entityLogicalName, createFromEntity, parameters).then(successCallback, errorCallback);

  • entityLogicalName: имя объекта. Только он является обязаетльным для этой функции.
  • createFromEntity: JS-объект типа lookup. Если передать его, то получится как от эффекта создания связанных записей. Т.е. сработает маппинг и заполнятся какие-либо поля.
  • parameters: принмает JS-объект с дополнитеьными параметрами. Например посредством него Вы можете предзаполнить определенноые поля на форме быстрого создания.
  • successCallback: функция, которая будет вызвана после успешного создании записи. В функцию передается объект-lookup успешно созданной записи.
  • errorCallback: функция, которая будет вызвана при возникновении ошибки.

Пример:

var parrentAccount = {
    entityType: "account",
    id: Xrm.Page.data.entity.getId()
};

var parameters = {
    name: "Child account of " + Xrm.Page.getAttribute("name").getValue(),
    address1_postalcode: Xrm.Page.getAttribute("address1_postalcode").getValue()
};

Xrm.Utility.openQuickCreate(
    "account",
    parrentAccount,
    parameters
).then(
    function (lookup) { successCallback(lookup); },
    function (error) { errorCallback(error); }
);

function successCallback(lookup) {
    window.console.log("lookup: " + lookup.savedEntityReference.id);
}
function errorCallback(e) {
    window.console.log("Error: " + e.errorCode + " " + e.message);
}

openEntityForm

Xrm.Utility.openEntityForm обзавелась новыми параметрам windowOptions, который позволяет открывать форму в новом окне браузера.

var windowOptions = {
    openInNewWindow: true
};
Xrm.Utility.openEntityForm("account", null, null, windowOptions);

FormFactor

Метеод Xrm.Page.context.client.getFormFactor подскажет Вам является текущее устройство обычны компьтером, планшетом или телефоном.

Поля даты/времени

Обзавелись методом Control.getShowTime() и setShowTime(), который позволяет определять и управлят тем, выведен ли в настоящее время компонент времени у даты/времени.

Keypress Events

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

Xrm.Page.getControl('telephone1').addOnKeyPress(function () {
    var userInput = Xrm.Page.getControl('telephone1').getValue();
    userInput = userInput.replace(/[^\d-()]+/g, '');
    Xrm.Page.getAttribute('telephone1').setValue(userInput);
});

Здесь мы вешаем обработку события на поле telephone1. После того как триггер сработал, получаем текущее значение, «очищаем» его с помощью регулярного выражения и записываем обратно. Т.о. пользователь никогда не введет запрещенные символы.

Сфера применения очень большая здесь: форматирование значений по оперделенному шаблону (например, номеров телефонов) или функции автозаполнения и т.д.
Чтобы удалить обработчик с поля необходим использовать функцию removeOnKeyPress:

Xrm.Page.getControl(fieldName).removeOnKeyPress(functionName);

З.Ы. С помощью этого метода нельзя удалить анонимную функцию.

А чтобы принудительно запустить обработчик используейте такой метод:

Xrm.Page.getControl(fieldName).fireOnKeyPress();

AutoComplete

В CRM 2016 появилась замечательная функция AutoComplete. Для тех, кто не вкурсе, это когда Вы печатаете тект а в ниспадающем меню тут же появляются подсказки – какие значения могут быть. Вы можете щелкнуть по одному из них и оно подставится в текстовое поле. По внешнему виду контрол один в один похож на ниспадающий список лукапа.
Функционал настраивается только программным способом. Чтобы показать подсказки используется следующий метод:

Xrm.Page.getControl(field name).showAutoComplete(object);

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

var resultset = {
    results: [{
        id: ,
        icon: ,
        fields: []}],
    commands:{
        id: ,
        icon: ,
        label: ,
        action:
        }
}

Посмотрим на полный пример:

  • Создайте JS Веб-ресурс с таким кодом:
    function onLoad() {
        // Полный список подсказок
        hints = [
        { name: 'Редиска', code: 'A01' },
        { name: 'Картофель', code: 'A02' },
        { name: 'Капуста', code: 'A03' },
        { name: 'Морковь', code: 'A04' },
        { name: 'Редис', code: 'A05' },
        { name: 'Вишня', code: 'A06' },
        { name: 'Малина', code: 'A07' },
        { name: 'Клубника', code: 'A08' },
        { name: 'Черешня', code: 'A09' },
        { name: 'Петрушка', code: 'A10' },
        { name: 'Дыня', code: 'A11' }
        ];
    
        // Функция будет выполняться каждый раз при нажатии кнопки
        var localityKeyPress = function (ext) {
            var userInput = Xrm.Page.getControl("name").getValue();
    
            // Очищаем массив с подсказками
            resultSet = {
                results: new Array(),
                commands: {
                    id: "sp_commands",
                    label: "Поиск", // Help link text
                    action: function () {
                        window.open("http:/mmcrm.ru"); // Help link URL
                    }
                }
            };
    
            // Сравниваем введенный текст с полным списком подсказок и если начальные значения совпадат - добавляем в массив подсказок
            var userInputLowerCase = userInput.toLowerCase();
            for (i = 0; i &lt; hints.length; i++) {
                if (userInputLowerCase === hints[i].name.substring(0, userInputLowerCase.length).toLowerCase()) {
                    resultSet.results.push({
                        id: i,
                        fields: [hints[i].name]
                        //, icon:&lt;url&gt; — Its an option field. You can show icon next to the Auto populated text.
                    });
                }
                if (resultSet.results.length &gt;= 10) break;
            }
            if (resultSet.results.length &gt; 0) {
                // Если совпадений найдены - отображаем ниспадающий список
                ext.getEventSource().showAutoComplete(resultSet);
            } else {
            // Иначе скрываем ниспадающий список
        ext.getEventSource().hideAutoComplete();
        }
    };
    
    // Добавляем к полю обработчик нажатия кнопки
    Xrm.Page.getControl("name").addOnKeyPress(localityKeyPress);
    }
    
  • Подключите его к форме на онлоад (в данном случае это Организация).


Примечания:

  • Функция AutoComplete показывает до 10 подсказок за раз.
  • AutoComplete не работает на планшетах и телефонах.
  • После выбора значения автоматически срабатывает событие OnChange соответствующего поля.

Optimistic concurrency

В многопоточной и многопользовательской системе, коей является Microsoft Dynamics CRM, операции изменения данных часто происходят параллельно. Проблема возникает, когда два или больше потока одновременно обновляют или удаляют одни и те же данные. Эта ситуация потенциально может привести к потере или искажению данных.

Чтобы снизить потери данных посредством SDK, MS ввела новую функциональность — Optimistic concurrency. Эта функциональность позволит Вам определить, были ли изменены данные, которые Вы собираетесь обновить, в промежутке, между тем как Вы их получили и собственно попыткой обновления. Если данные были изменены, то Вы можете запросить их актуальную версию и повторно попытаться обновить.

Эта функциональность использует значение RowVersion строки БД, чтобы определить, были ли изменены данные в промежутке между запросами (возвращения и обновления). А свойство ConcurrencyBehavior сообщений UpdateRequest и DeleteRequest определяет поведение оптимистичного параллелизма, которое должно быть выполнено веб-сервисом CRM при обработке этих запросов. Возможные значения:

  • AlwaysOverwrite – всегда перезаписывать. Данное поведение действует всегда для тех объектов, для которых оптимистичный параллелизм не применим.
  • Default – значение по умолчанию. Может быть различным в зависимости от контекста вызова.
  • IfRowVersionMatches – обновление произойдет, только если у записи в БД та же версия, что Вы передали.

А теперь посмотрим на примемеры…

.Net

using (var service = new OrganizationService(crmConnection))
{
    var account = service.Retrieve("account", accountId, new ColumnSet("name", "revenue", "creditlimit"));

    object revenue;
    if (account != null && account.Attributes.TryGetValue("revenue", out revenue) && ((Money)revenue).Value > 50000000)
    {
        var updatedAccount = new Entity()
        {
            LogicalName = account.LogicalName,
            Id = account.Id,
            RowVersion = account.RowVersion
        };

        updatedAccount["creditlimit"] = new Money(5000);

        // Определяем поведение - только при совпадении версий будем обновлять данные
        var accountUpdate = new UpdateRequest()
        {
            Target = updatedAccount,
            ConcurrencyBehavior = ConcurrencyBehavior.IfRowVersionMatches
        };
 
        try
        {
            var accountUpdateResponse = (UpdateResponse)service.Execute(accountUpdate);
        }
        catch (FaultException<OrganizationServiceFault> ex)
        {
            if (ex.Code == OPTIMISTIC_CONCURRENCY_VIOLATION)
            {
                // Если не совпали версии - что-то делаем...
            }
        }
    }
}

Обработка ошибок осуществляется следующим образом:

catch (FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> e)
{
    if (e.Detail.ErrorCode == CrmSdk.ErrorCodes.ConcurrencyVersionMismatch)
    {
        var account = _serviceProxy.Retrieve("account", _accountId, new ColumnSet());
        throw new Exception(String.Format("A mismatched row version ({0}) caused the request to fail.", account.RowVersion), e);
    }
    else    
        throw e;
}

А коды ошибок могут быть такими:

  • ConcurrencyVersionMismatch (code=-2147088254) – возвращается, когда версии не совпадают.
  • ConcurrencyVersionNotProvided (code= -2147088253) – возврвщается, когда номера версий не возможно сравнить (из-за отсутствия оных).
  • OptimisticConcurrencyNotEnabled (code=-2147088243) – возвращается, когда оптимистичная конкурентция не включена для объекта.
    • JavaScript

      Optimistic Concurrency можно использовать и с Web API (про него читайте в следующих выпусках). Каждый раз при запросе данных ответ будет содержать @odata.etag – значение этого свойства обновляется каждый раз при обновлении объекта. Далее в заголовке запроса If-Match передаем ранее полуенное @odata.etag, чтобы убедится, что данные небыли изменены с последнего запроса. Если вернется статуса с кодом 412, значинт ваша версия данных отличается от серверной и данные не будут обновлены. Если код статуса будет 204, то обновление прошло удачно и версии совспли.

      function updateRecord() {
          var clientURL = Xrm.Page.context.getClientUrl();
          var accountId = "f26b5f92-5798-e511-80e3-3863bb2ead80";
          var req = new XMLHttpRequest()
          req.open("PATCH", encodeURI(clientURL + "/api/data/v8.0/accounts(" + accountId + ")"), true);
          req.setRequestHeader("If-Match", "W/\"632353\"");
          req.setRequestHeader("Accept", "application/json");
          req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
          req.setRequestHeader("OData-MaxVersion", "4.0");
          req.setRequestHeader("OData-Version", "4.0");
          req.onreadystatechange = function () {
              if (this.readyState == 4) {
                  req.onreadystatechange = null;
                  if (this.status == 204) {
                      var data = JSON.parse(this.response, dateReviver);
                  }
                  else if (this.status == 412) {
                      var error = JSON.parse(this.response).error;
                      alert("Precondition Failed – " + error.message);
                  }
                  else {
                      var error = JSON.parse(this.response).error;
                      alert("Error updating Account – " + error.message);
                  }
              }
          };
          // Set Account record properties
          req.send(JSON.stringify({ name: "Rajeev Pentyala" }));
      }
      function dateReviver(key, value) {
          var a;
          if (typeof value === 'string') {
              a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
              if (a) {
                  return new Date(Date.UTC(+a[1], +a[2] – 1, +a[3], +a[4], +a[5], +a[6]));
              }
          }
          return value;
      };
      

      C#

      Транзации

      Целостность данных – одна самых критически важныъ вещей для бизнеса. А транзакции – в один из способов гарантировать целостность данных. Ранее тразакции поддерживались только в плагинах CRM. Теперь же Вы можете инициировать их в своих кастомных приложениях.

      ExecuteTransactionRequest очень похож на ExecuteMultipleRequest за тем исключением, что Execute Multiple создает отдельную транзакцию на каждый запрос в пакете. В свою очередь Execute Transaction работает в единственной транзакции.

      Пример создания 3 записей в одной транзации. Если не будет создана хотя бы одна зпись, то система откатит всю транзакцию.

      using (var service = new OrganizationService(crmConnection))
      {
          var request = new ExecuteTransactionRequest()
          {
              ReturnResponses = true,
              Requests = new OrganizationRequestCollection()
          };
       
          for (int i = 1; i <= 3; i++)
          {
              var account = new Entity("account");
              account["name"] = string.Format("Test Account {0}", i);
      
              var createRequest = new CreateRequest() { Target = account };
              request.Requests.Add(createRequest);
          }
       
          var response = (ExecuteTransactionResponse)service.Execute(request);
       
          foreach (var responseItem in response.Responses)
          {
              var createResponse = (CreateResponse)responseItem;
              Console.WriteLine("Account created: {0}", createResponse.id);
          }
      }
      

      Ответ этого запроса является набором ответов каждого отдельного запроса, включенного в пакет. Сообщения выполняются в той последоватльности, в какой включены в пакет. В таком же порядке возвращаются ответы (первое сообщение имеет индекс – 0).

      При возникновении ошибки при выполнении транзации система вернет исключение ExecuteTransactionFault, который возвратит номер сообщения которое свалилоь с ошибкой:

      catch (FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> ex)
      {
          ExecuteTransactionFault fault = (ExecuteTransactionFault)ex.Detail;
          int faultedIndex = fault.FaultedRequestIndex;
      }
      

      Примечания

      • ExecuteTransactionRequest может входить в состав сообщения ExecuteMultipleRequest.
        • В этом случае размер пакета ExecuteMultiple вычислен общим количеством всех отдельных элементов.
      • ExecuteTransactionRequest не может включать в себя сообщения ExecuteTransactionRequest или ExecuteMultipleRequest.
      • ExecuteTransactionRequest ограничен теми же пределами, что и ExecuteMultipleRequest:
        • Максимальный размер пакета – 1000.
        • CRM Online имеет ограничение на 2 одновременных пакетных запроса.
      • Используя ExecuteTransactionRequest помините, что выполнение такого запроса занимает больше времени и может блокировать блокировать другие операции в системе.

      Трассировка

      MS внесло улучшение в Tracing Service, которое значительно упрщяет жизнь разработчиков во время отладки кода.

      Tracing Service была представлена в CRM 2011 и она позволяет разработчикам писать отладочные сообщения из плагинов и кастмоных БП. Сами логи писались только в Event Log сервера CRM, а таже включались в сообщения об ошибке, отображаемое пользователю (т.е. это требовало чтобы они присылали Вам лог). Проблема заключалась в том, что Event Log не доступен для CRM Onlline. А для локальных развертываний необходимо постоянно заходить на сервер и скать сообщение в Event Log среди сотен других.

      Многие разрвботчики, чтобы поправить это писали сообщения в кастомные объекты, чтобы легко получить к ним доступ из интерфейса CRM. Но и тут не обходилось без проблем. При откате транзакции все записи, добавленные в такие кастомные объекты, также удалялись.

      Новое улучшения Tracing Service нивилирует эти недостатки, записывая сообщения в новый стандартный объект – Журнал трассировки подключаемого модуля, доступный через пользовательский интерфейс CRM. Они пишутся туда даже в случае отката транзакции.

      З.Ы. По каким-то непонятным причинам в On-premis данный механизм пока работает только для sandbox-плагинов.

      Чтобы включить CRM’ную трассировку:

      • Перейдите Параметры – Администрирование – Системные параметры – вкладка Настройка;
      • Выберите один из трех вариантов трассировки в ниспадающем списке:
        • Выкл – без комментарие 🙂
        • Исключения – в лог будут записываться только необработанные исключения в Ваших плагинах и БП;
        • Все – в лог будут записываться все вызовы метода tracingService.Trace (tracingService это экземпляр ITracingService) или исключения.

      Чтобы просмотреть зарегистрированные журналы перейдите Параметры – Журналы трассировки. В каждой записи Вы сможете найти следующую информацию:

      • Cообщение переданное методу Trace.
      • Имя объекта.
      • Внутреннее сообщение об исключении.
      • Глубину контекста выполнения.
      • Продолжительность выполнения.
      • Безопасная/небезопасная конфигурация.
      • И т.д.


      Вы не можете кастомизировать объект Plug-in Trace Log или информацию, записываемую в него. Вы можете только либо посмотреть записи логов, либо удалить их.

      Удаление журналов

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

      • Перейдите Праметры – Массовое удаление записей – смените Представление на Повторяющиеся системные задания группового дуления.
      • Откройте задание Удалить записи журнала трассировки подключаемых модуле – Действия – Изменение интервала повторения.
      • Измениет параметры запуска в соответствии с вашими потребностями.

      Версия CRM

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

      З.Ы. Это немного напоминает верстку под различные браузеры, с разной функциональностью.

      Чтобы определить версию Вы можете использовать новое сообщение – RetrieveVersionRequest:

      using (var service = new OrganizationService(crmConnection))
      {
          var request = new RetrieveVersionRequest();
          var response = (RetrieveVersionResponse)service.Execute(request);
          var version = new Version(response.Version);
       
          if (version >= new Version("7.1"))
          {
              // Do the new way
          }
          else
          {
              // Do the old way
          }
      }
      

      Прочее

      SetProcessRequest

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

      var service = GetService();
      SetProcessRequest req = new SetProcessRequest();
      req.Target = new EntityReference("opportunity", Guid.Parse("7059510E-59A3-E511-80E4-3863BB35AD90");
      req.NewProcess = new EntityReference("process", Guid.Parse("AEC906DF-5C10-490D-8D0E-E5620FAAE614");
      var resp = service.Execute(req);
      

      GetDefaultPriceLevelRequest

      Как понятно из названия, возвращает дефолтный прайс-лись для текущего пользователя в соответствии с его территориальными настройками.

Комментарии (1)
  • magicandy 22.08.2016

    Отлично! Спасибо. Ждём эпизод 2.

*

code