Комментарии 20
А почему не решились перейти на 4.0.3?
Проблему с bookmarks решил достаточно просто: когда устанавливаю закладку, то сохраняю мета информацию в базу (всё в транзакции).
Проблему с определением того, кто может нажать — в мета информации сохраняется ид пользователя, который может нажать на данную кнопку.
Мета информация примерно следующая:
— название кнопки;
— кто может нажать;
— срок;
— права, которая дает кнопка;
— валидации (например, что должны быть заполнены определённые поля);
— форма запрос (комментария, файла обонования, каких-нибудь пользователей и т. п.).
Соответственно одним запросом по базе можно узнать, кто, что и где может нажать.
Проблему с определением кто ещё должен согласовать и изменением перечня согласующих удалось решить, отказавшись от выделения состояния для каждого вида согласования. Есть просто статус Согласование, а перечень лиц, вообще определяется в настройках системы (или задается пользователем). Соответственно, если нужно подкорректировать процесс, пользователь заходит в настройки и указывает других согласующих лиц. Кстати, это так же позволило сильно снизить количество изменений в процессе, так как его частая изменяемая часть, вообще вынесена в настройки.
Единственное, что красиво пока не удалось решить, это вложенное согласование, так как WWF не поддерживает рекурсии в процессах.
Проблему с bookmarks решил достаточно просто: когда устанавливаю закладку, то сохраняю мета информацию в базу (всё в транзакции).
Проблему с определением того, кто может нажать — в мета информации сохраняется ид пользователя, который может нажать на данную кнопку.
Мета информация примерно следующая:
— название кнопки;
— кто может нажать;
— срок;
— права, которая дает кнопка;
— валидации (например, что должны быть заполнены определённые поля);
— форма запрос (комментария, файла обонования, каких-нибудь пользователей и т. п.).
Соответственно одним запросом по базе можно узнать, кто, что и где может нажать.
Проблему с определением кто ещё должен согласовать и изменением перечня согласующих удалось решить, отказавшись от выделения состояния для каждого вида согласования. Есть просто статус Согласование, а перечень лиц, вообще определяется в настройках системы (или задается пользователем). Соответственно, если нужно подкорректировать процесс, пользователь заходит в настройки и указывает других согласующих лиц. Кстати, это так же позволило сильно снизить количество изменений в процессе, так как его частая изменяемая часть, вообще вынесена в настройки.
Единственное, что красиво пока не удалось решить, это вложенное согласование, так как WWF не поддерживает рекурсии в процессах.
WWF поддерживает делегаты, а делегат можно вызвать и рекурсивно, если очень хочется.
Сейчас точно не скажу, но вроде проблемы были следующие:
— делегату нужно подсунуть отдельно созданную активити, а жутко не хотелось процесс дробить на несколько файлов;
— не уверен, работают ли закладки во вложенных процессах, вроде как запускает отдельный процесс.
— делегату нужно подсунуть отдельно созданную активити, а жутко не хотелось процесс дробить на несколько файлов;
— не уверен, работают ли закладки во вложенных процессах, вроде как запускает отдельный процесс.
делегату нужно подсунуть отдельно созданную активити, а жутко не хотелось процесс дробить на несколько файлов;Зачем? Не понимаю вас.
не уверен, работают ли закладки во вложенных процессах, вроде как запускает отдельный процесс.Аналогично: о каких вложенных процессах идет речь, если делегат выполняется в основном?
Когда появился WF 4 в нем не было State Machine совсем, он не совместим с 3.5. А самое главное он не решал большей части проблем, а только добавлял новые.
>>Проблему с определением того, кто может нажать — в мета информации сохраняется ид пользователя, который может нажать на данную кнопку.
А что делать там не id пользователя, а условие? Например, согласовать может только сотрудник с ролью Куратор из родительского подразделения или в случае отсутствия куратора согласует начальник департамента.В этом случае обычным запросом к БД не обойтись. Так же это существенно усложняет мета данные и их обработку.
Можете выложить код, как проводите обработку мета-данных?
>>Согласование, а перечень лиц, вообще определяется в настройках системы (или задается пользователем).
Иногда мы делаем так же, но это не лучший вариант.
>>Единственное, что красиво пока не удалось решить, это вложенное согласование, так как WWF не поддерживает рекурсии в процессах.
У нас пока тоже это не реализовано. По плану должны сделать в марте. Я сейчас готовлю статью на тему «Подпроцессы в workflow и паралельное согласование». Статья будет опубликована на codeproject.com.
>>Проблему с определением того, кто может нажать — в мета информации сохраняется ид пользователя, который может нажать на данную кнопку.
А что делать там не id пользователя, а условие? Например, согласовать может только сотрудник с ролью Куратор из родительского подразделения или в случае отсутствия куратора согласует начальник департамента.В этом случае обычным запросом к БД не обойтись. Так же это существенно усложняет мета данные и их обработку.
Можете выложить код, как проводите обработку мета-данных?
>>Согласование, а перечень лиц, вообще определяется в настройках системы (или задается пользователем).
Иногда мы делаем так же, но это не лучший вариант.
>>Единственное, что красиво пока не удалось решить, это вложенное согласование, так как WWF не поддерживает рекурсии в процессах.
У нас пока тоже это не реализовано. По плану должны сделать в марте. Я сейчас готовлю статью на тему «Подпроцессы в workflow и паралельное согласование». Статья будет опубликована на codeproject.com.
>>А что делать там не id пользователя, а условие?
1. Только сотрудник с ролью Куратор.
Указываем в метаинформации, не конкретного сотрудника, а группу/роль и т. п. Хотя я так не делают, так как согласование должно проходит достаточно быстро (день, неделя, месяц). Поэтому маловероятно, что роль будет меняться так часто. Поэтому указываю конкретного человека из настроек/по условию (прям в wwf) на момент создания кнопки.
Если, что-то изменилось очень срочно, то есть два стандартных механизма для решения таких проблем:
— Замещение.
— Изменить ответственного через службу поддержки (в данный момент это 1 человек на, где-то, 700 ежедневных уникальных пользователей системы, к сожалению, общее количество работающих с системой не знаю). Пока справляется, даже совмещать с другой должностью успевает.
2. В случае отсутствия куратора согласует начальник департамента
Решается через общий механизм замещения.
3. Можете выложить код, как проводите обработку мета-данных?
Выбрать все кнопки, где пользователь является ответстенным. Плюс выбрать все кнопки, где ты замещаешь ответственного. Вот и получается перечень твоих задач.
4. Иногда мы делаем так же, но это не лучший вариант.
Почему не лучший? Я даже маленький парсер сделал, чтобы можно было задавать не только последовательное согласование, но и смешанное.
Пользователь1, Пользователь2=Пользователь3, Пользователь4
Почти все достаточно легко разобрались в этом и очень активно используют.
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) нужно потратить пару недель (при этом документооборот у вас будет задаваться в разных местах код — это прямой путь к ошибках).
У нас список доступных команд определяется вызовом одного метода — GetAvailableCommands.
>>1.Указываем в метаинформации, не конкретного сотрудника, а группу/роль
Это приведет к усложнению вашей мета информации. Простым SQL фильтром не обойтись при выборке команд.
>>2. В случае отсутствия куратора согласует начальник департамента
Решается через общий механизм замещения.
В этом случае начальник департамента будет видеть лишние документы. Это не всегда подходит.
Не лучший потому что подходит только для простейших случаев, если процессы согласования в будущем будут усложняться, то прийдется переделывать.
Что бы сделать механизм получения списка доступных команд в WF 3.5(4) нужно потратить пару недель (при этом документооборот у вас будет задаваться в разных местах код — это прямой путь к ошибках).Я это сделал за день… что я сделал не так?)
Код вашей реализации в студию :)
Скопировать не могу, я дома, а код на работе, да и не открытый код мы пишем (по крайней мере, до тех пор пока продукт не будет готов).
Но сделано примерно так. Объясняю на примере примитива «кнопка». Кнопка выводится в определенном месте на сайте и ждет там, пока ее нажмет нужный пользователь.
У кнопки два параметра — Code и Text. Первый отвечает за семантику сигнала, второй — за отображение пользователю. В принципе, в условиях готового проекта, когда каждая кнопка на своем месте, параметр Text избыточен — но на этапе разработки возможность «бросить» Activity в процесс и сразу же его увидеть на сайте без дополнительной настройки вида — бесценна.
Также, раз речь зашла о правах доступа, добавлю аргумент Actor, определяющий того пользователя, которому эта кнопка предназначена — другие пользователи не только не смогут нажать ее, но и даже увидеть.
Задача определения списка допустимых действий здесь решается в два этапа: сначала определяется, в каких инстансах процессов актора ожидают какие-либо действия, а загрузив конкретный процесс можно узнать и сам список.
Если нажать на кнопку могут несколько акторов — то и экземпляров ButtonActivity вызывается одновременно несколько. Если какой-то актор не может нажать кнопку (был заменен) — выбрасывается исключение, которое приводит к повтору части рабочего процесса (в т.ч. логики выбора актора).
К счастью, задачи согласования заранее неопределенным кругом лиц не стояло. Но если бы такая задача стояла, причем требовались бы условия произвольной сложности, я бы посмотрел в сторону автоматического определения зависимостей.
Осталось реализовать ChangesDetectionContext и ObserverService. Это не так сложно, как могло бы показаться — по крайней мере, это точно проще атомов, ведь тут нет зависимостей свойств друг от друга.
Как это работает? ObserveAndExecuteActivity состоит из трех частей: селектора, триггера и тела. Сначала отрабатывает селектор, на вход ему подается новый экземпляр ChangesDetectionContext, а на выходе имеем некоторое количество сущностей (возможно, одну). При этом все свойства, к которым обращалась логика, прописанная в селекторе, оказались записаны в ChangesDetectionContext и потом в базу. Если какое-то из них изменится, то селектор будет запущен снова.
Закладку, которая будет возобновлена для повторного запуска селектора, я для разнообразия не стал сохранять в Extension, а просто задал ей имя.
После того, как селектор отработал, запускается по одному экземпляру триггера на каждую возвращенную им сущность. Триггер — это может быть примитив кнопки, получения сообщения, системного события — или любая их комбинация. Смысл триггера в том, что различные экземпляры триггера «соревнуются» друг с другом, кто первее отработает. Если селектор отработал не в первый раз, то, возможно, некоторые старые экземпляры триггера отменяются.
Как только один из триггеров успешно завершился — остальные триггеры отменяются, и селектор больше вызываться тоже не будет. Для сущности, связанной с «победившим» триггером, вызывается тело, выполнение которого уже не может быть прервано. Тело — это уже необязательная часть, в некоторых случаях все нужная работа может быть совершена еще в триггере.
PS код писал сразу в браузере, не проверяя — прошу не обижаться, если он содержит ошибки.
Но сделано примерно так. Объясняю на примере примитива «кнопка». Кнопка выводится в определенном месте на сайте и ждет там, пока ее нажмет нужный пользователь.
У кнопки два параметра — 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, чем добавлять механизм сохранения в БД.
Нам было легче сохранять в виде отдельных DLL, чем добавлять механизм сохранения в БД.
Как раз сейчас стоит вопрос — реализовать ли свой движок или взять WF…
Видимо, все-таки, свой) проблема динамического добавления состояний — очень актуальная проблема в нашем случае.
Видимо, все-таки, свой) проблема динамического добавления состояний — очень актуальная проблема в нашем случае.
Взгляните на наш движок документооборота. Возможно, он подойдет для решения ваших задач.
Справедливости ради, динамическое добавление состояний в WWF работает «из коробки». Проблемы начинаются при их динамическом изменении.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Workflow в Document Approval System