Pull to refresh

Comments 20

А почему не решились перейти на 4.0.3?

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

Мета информация примерно следующая:
— название кнопки;
— кто может нажать;
— срок;
— права, которая дает кнопка;
— валидации (например, что должны быть заполнены определённые поля);
— форма запрос (комментария, файла обонования, каких-нибудь пользователей и т. п.).

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

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

Единственное, что красиво пока не удалось решить, это вложенное согласование, так как WWF не поддерживает рекурсии в процессах.
WWF поддерживает делегаты, а делегат можно вызвать и рекурсивно, если очень хочется.
Сейчас точно не скажу, но вроде проблемы были следующие:
— делегату нужно подсунуть отдельно созданную активити, а жутко не хотелось процесс дробить на несколько файлов;
— не уверен, работают ли закладки во вложенных процессах, вроде как запускает отдельный процесс.

делегату нужно подсунуть отдельно созданную активити, а жутко не хотелось процесс дробить на несколько файлов;
Зачем? Не понимаю вас.

не уверен, работают ли закладки во вложенных процессах, вроде как запускает отдельный процесс.
Аналогично: о каких вложенных процессах идет речь, если делегат выполняется в основном?
Значит прошу прощения, надо вернуться к этому вопросу и должным образом разобраться.
Когда появился WF 4 в нем не было State Machine совсем, он не совместим с 3.5. А самое главное он не решал большей части проблем, а только добавлял новые.

>>Проблему с определением того, кто может нажать — в мета информации сохраняется ид пользователя, который может нажать на данную кнопку.
А что делать там не id пользователя, а условие? Например, согласовать может только сотрудник с ролью Куратор из родительского подразделения или в случае отсутствия куратора согласует начальник департамента.В этом случае обычным запросом к БД не обойтись. Так же это существенно усложняет мета данные и их обработку.
Можете выложить код, как проводите обработку мета-данных?

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

>>Единственное, что красиво пока не удалось решить, это вложенное согласование, так как WWF не поддерживает рекурсии в процессах.
У нас пока тоже это не реализовано. По плану должны сделать в марте. Я сейчас готовлю статью на тему «Подпроцессы в workflow и паралельное согласование». Статья будет опубликована на codeproject.com.

>>А что делать там не id пользователя, а условие?
1. Только сотрудник с ролью Куратор.
Указываем в метаинформации, не конкретного сотрудника, а группу/роль и т. п. Хотя я так не делают, так как согласование должно проходит достаточно быстро (день, неделя, месяц). Поэтому маловероятно, что роль будет меняться так часто. Поэтому указываю конкретного человека из настроек/по условию (прям в wwf) на момент создания кнопки.

Если, что-то изменилось очень срочно, то есть два стандартных механизма для решения таких проблем:
— Замещение.
— Изменить ответственного через службу поддержки (в данный момент это 1 человек на, где-то, 700 ежедневных уникальных пользователей системы, к сожалению, общее количество работающих с системой не знаю). Пока справляется, даже совмещать с другой должностью успевает.

2. В случае отсутствия куратора согласует начальник департамента
Решается через общий механизм замещения.

3. Можете выложить код, как проводите обработку мета-данных?
Выбрать все кнопки, где пользователь является ответстенным. Плюс выбрать все кнопки, где ты замещаешь ответственного. Вот и получается перечень твоих задач.

CREATE VIEW Tasks WITH SCHEMABINDING	
	AS
SELECT 
	b.Employee,	
	t.Card AS CardID,
	t.InstanceID As WWFID,
	i.Description as [Digest],
	i.CardTypeID AS CardTypeID,
	t.Status,
	d.CreationDateTime,
	COUNT_BIG(*) AS cBig
FROM dbo.[dvtable_{39857033-BDD0-4771-8654-482957FD1338}] as t
	INNER JOIN dbo.[dvtable_{E6CEA68A-49EE-4E5C-8F54-E73EFB7EA78F}] as b on b.InstanceID = t.InstanceID
	INNER JOIN dbo.dvsys_instances as i on i.InstanceID = t.Card
	INNER JOIN dbo.dvsys_instances_date as d on d.instanceid = i.InstanceID
WHERE (i.Deleted IS NULL or i.Deleted = 0) and i.template = 0
GROUP BY 
	b.Employee,	
	t.Card,
	t.InstanceID,
	i.Description,
	i.CardTypeID,
	t.Status,
	d.CreationDateTime
	
GO
CREATE UNIQUE CLUSTERED INDEX IDX_Tasks ON Tasks(Employee, CardID, WWFID);


4. Иногда мы делаем так же, но это не лучший вариант.
Почему не лучший? Я даже маленький парсер сделал, чтобы можно было задавать не только последовательное согласование, но и смешанное.

Пользователь1, Пользователь2=Пользователь3, Пользователь4

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

Что бы сделать механизм получения списка доступных команд в WF 3.5(4) нужно потратить пару недель (при этом документооборот у вас будет задаваться в разных местах код — это прямой путь к ошибках).
У нас список доступных команд определяется вызовом одного метода — GetAvailableCommands.

>>1.Указываем в метаинформации, не конкретного сотрудника, а группу/роль
Это приведет к усложнению вашей мета информации. Простым SQL фильтром не обойтись при выборке команд.

>>2. В случае отсутствия куратора согласует начальник департамента
Решается через общий механизм замещения.

В этом случае начальник департамента будет видеть лишние документы. Это не всегда подходит.

Не лучший потому что подходит только для простейших случаев, если процессы согласования в будущем будут усложняться, то прийдется переделывать.
Что бы сделать механизм получения списка доступных команд в WF 3.5(4) нужно потратить пару недель (при этом документооборот у вас будет задаваться в разных местах код — это прямой путь к ошибках).
Я это сделал за день… что я сделал не так?)
Скопировать не могу, я дома, а код на работе, да и не открытый код мы пишем (по крайней мере, до тех пор пока продукт не будет готов).

Но сделано примерно так. Объясняю на примере примитива «кнопка». Кнопка выводится в определенном месте на сайте и ждет там, пока ее нажмет нужный пользователь.

У кнопки два параметра — Code и Text. Первый отвечает за семантику сигнала, второй — за отображение пользователю. В принципе, в условиях готового проекта, когда каждая кнопка на своем месте, параметр Text избыточен — но на этапе разработки возможность «бросить» Activity в процесс и сразу же его увидеть на сайте без дополнительной настройки вида — бесценна.

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

Код кнопки
public class ButtonActivity : NativeActivity {
    [DisplayName("Код кнопки")]
    public string Code {get; set;}

    [DisplayName("Текст кнопки")]
    public string Text {get; set;}

    /* Один из недостатков стандартного дизайнера - невозможность понять, какой аргумент является входным, а какой выходным, без подглядывания в документацию. Поэтому я придумал вот так их маркировать */
    [DisplayName("[IN] Актор")]
    [RequiredArgument]
    public InArgument<string> Actor {get; set;}

    protected bool CanInduceIdle { get { return true; } }

    protected override void CacheMetadata(NativeActivityMetadata metadata) {
        base.CacheMetadata(metadata);
        metadata.RequireExtension<ButtonsExtension>();
        metadata.RequireExtension<ActorsService>();
        if (string.IsNullOrEmpty(Code)) metadata.AddValidationError("Не указан код кнопки");
        if (string.IsNullOrEmpty(Text )) metadata.AddValidationError("Не указан текст кнопки");
    }

    protected override void Execute(NativeActivityContext context) {
        var bm = context.CreateBookmark(context.ActivityInstanceId);
        context.GetExtension<ButtonsExtension>().RegisterButton(context.ActivityInstanceId, Code, Text, Actor.Get(context), bm);
        context.GetExtension<ActorsService>().Link(context.WorkflowInstanceId, context.ActivityInstanceId, Actor.Get(context));
    }

    private void OnFinish(NativeActivityContext context) {
        context.GetExtension<ButtonsExtension>().UnregisterButton(context.ActivityInstanceId);
        context.GetExtension<ActorsService>().Unlink(context.WorkflowInstanceId, context.ActivityInstanceId);
    }

    protected override void Cancel(NativeActivityContext context) {
        base.Cancel(context);
        OnFinish(context);
    }

    protected override void Abort(NativeActivityAbortContext context) {
        base.Abort(context);
        OnFinish(context);
    }

    private void OnResume(NativeActivityContext context, Bookmark bm, object value) {
        OnFinish(context);

        if (value is Exception) throw (Exception)value;
    }
}

public class ButtonsExtension : PersistenceParticipant {
    const string qname = "{my-namespace}Buttons";
    private List<ButtonInfo> activeButtons = new List<ButtonInfo>();

    public void RegisterButton(string activityInstanceId, string code, string text, string actor, Bookmark bm) {
        activeButtons.Add(new ButtonInfo { ActivityInstanceId = activityInstanceId, Code = code, Text = text, Actor = actor, Bookmark = bm });
    }

    public void UnregisterButton(string activityInstanceId) {
        activeButtons.RemoveAll(b => b.ActivityInstanceId  == activityInstanceId);
    }

    public IEnumerable<ButtonInfo> GetActiveButtons() {
        return activeButtons;
    }

    protected override void CollectValues(out IDictionary<XName, object> readWriteValues, out IDictionary<XName, object> writeOnlyValues) {
        readWriteValues = new Dictionary<XName, object> { { qname, activeButtons } };
        writeOnlyValues = null;
    }

    protected override void PublishValues(IDictionary<XName, object> readWriteValues) {
        activeButtons = (List<ButtonInfo>)readWriteValues[qname];
    }
}

public class ActorsService; // Этот класс просто работает с БД, его реализация не интересна

public class ButtonInfo {
    public string ActivityInstanceId {get; set;}
    public string Code {get; set;}
    public string Text {get; set;}
    public string Actor {get; set;}
    public Bookmark Bookmark {get; set;}
}



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

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

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

Как-то так
public class ObserveAndExecuteActivity<T> {
    public ActivityFunc<ChangesDetectionContext, IEnumerable<T>> Selector {get; set;}
    public ActivityAction<T> Trigger {get; set;}
    public ActivityAction<T> Body {get; set;}

    private Variable<ChangesDetectionContext> cdContext = new Variable<ChangesDetectionContext>("cdContext");
    private Variable<Bookmark> selectionBookmark = new Variable<Bookmark>("selectionBookmark");
    private Variable<Dictionary<T, ActivityInstance>> triggerInstances = new Variable<Dictionary<T, ActivityInstance>>("triggerInstances", new Dictionary<T, ActivityInstance>());
    
    protected override void CacheMetadata(NativeActivityMetadata metadata) {
        base.CacheMetadata(metadata);
        metadata.RequireExtension<ObserverService>();
        metadata.AddImplementationVariable(cdContext);
        metadata.AddImplementationVariable(triggerInstances);
        metadata.AddImplementationVariable(selectionBookmark);
    }

    protected override void Execute (NativeActivityContext context) {
        StartSelector(context);
    }

    private void StartSelector(NativeActivityContext context) {
        var cdctx = new ChangesDetectionContext();
        cdContext.Set(context, cdctx);
        context.ScheduleFunc(Selector, cdctx, OnSelectorComplete);
    }

    private void OnSelectorComplete(NativeActivityContext context, ActivityInstance instance, IEnumerable<T> value) {
        if (instance.State == ActivityInstanceState.Cancelled) return;

        selectionBookmark.Set(context, context.CreateBookmark(context.ActivityInstanceId, OnReselect));
        context.GetExtension<ObserverService>().SetDeps(context.WorkflowInstanceId, context.ActivityInstanceId, cdContext.Get(context).Detect());
        cdContext.Set(context, null);

        var triggers = triggerInstances,Get(context);
        foreach (var v in value.Except(triggers.Keys).ToList())
            triggers.Add(v, context.ScheduleAction(Trigger, v, OnTriggerComplete);
        foreach (var v in triggers.Keys.Except(values).ToList()) {
            context.CancelChild(triggers[v]);
            triggers.Remove(v);
        }
    }

    private void OnReselect(NativeActivityContext, Bookmark bm, object value) {
        selectionBookmark.Set(context, null);
        StartSelector(context);
    }

    private void OnTriggerComplete(NativeActivityContext context, ActivityInstance instance) {
        if (instance.State == ActivityInstanceState.Cancelled) return;
        
        context.CancelBookmark(selectionBookmark.Get(context));
        selectionBookmark.Set(context, null);

        context.CancelChildren();
        var v = triggerInstances.Get(context).Single(pair => pair.Value == instance).Key;
        triggerInstances.Set(context, null);
        
        context.GetExtension<ObserverService>().Unregister(context.WorkflowInstanceId, context.ActivityInstanceId);
        context.ScheduleAction(Body, v, OnBodyComplete);
    }

    protected override void Cancel(NativeActivityContext context) {
        base.Cancel(context);
        context.GetExtension<ObserverService>().Unregister(context.WorkflowInstanceId, context.ActivityInstanceId);
    }

    protected override void Abort(NativeActivityAbortContext context) {
        base.Abort(context);
        context.GetExtension<ObserverService>().Unregister(context.WorkflowInstanceId, context.ActivityInstanceId);
    }
}



Осталось реализовать ChangesDetectionContext и ObserverService. Это не так сложно, как могло бы показаться — по крайней мере, это точно проще атомов, ведь тут нет зависимостей свойств друг от друга.

Как это работает? ObserveAndExecuteActivity состоит из трех частей: селектора, триггера и тела. Сначала отрабатывает селектор, на вход ему подается новый экземпляр ChangesDetectionContext, а на выходе имеем некоторое количество сущностей (возможно, одну). При этом все свойства, к которым обращалась логика, прописанная в селекторе, оказались записаны в ChangesDetectionContext и потом в базу. Если какое-то из них изменится, то селектор будет запущен снова.

Закладку, которая будет возобновлена для повторного запуска селектора, я для разнообразия не стал сохранять в Extension, а просто задал ей имя.

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

Как только один из триггеров успешно завершился — остальные триггеры отменяются, и селектор больше вызываться тоже не будет. Для сущности, связанной с «победившим» триггером, вызывается тело, выполнение которого уже не может быть прервано. Тело — это уже необязательная часть, в некоторых случаях все нужная работа может быть совершена еще в триггере.

PS код писал сразу в браузере, не проверяя — прошу не обижаться, если он содержит ошибки.
Хороший пример. Но всё же мне не верится, что разработчик, который делает это впервые, может за день это реализовать.
Первую часть — может (я же смог). Вторую часть — нет, за день у начинающего не выйдет.
Не вполне понимаю, зачем вы хранили разные версии рабочего процесса в виде отдельных DLL — если их можно хранить в формате XAML в той же базе данных.
У нас изначально было воркфлоу с кодом (тут).
Нам было легче сохранять в виде отдельных DLL, чем добавлять механизм сохранения в БД.
Как раз сейчас стоит вопрос — реализовать ли свой движок или взять WF…
Видимо, все-таки, свой) проблема динамического добавления состояний — очень актуальная проблема в нашем случае.
Справедливости ради, динамическое добавление состояний в WWF работает «из коробки». Проблемы начинаются при их динамическом изменении.
Можете поделится работающим примером? Я добавлю эту информацию в статью.
Разве что на выходных. Не хочется почему-то ночью ставить SQL Server…
Sign up to leave a comment.

Articles