• Icon: Feature Request Feature Request
    • Resolution: Fixed
    • Icon: L3 - Default L3 - Default
    • 7.12.0, 7.12.0-alpha5
    • None
    • engine
    • None

      I've worked with the "Activiti-family" of process engines for over 10 years: from jBPMN to Activiti, Flowable and recently Camunda. So far I've always written my own form handling functionality using custom commands.

      My intention now is to work with instead of around the workflow engine as much as possible, so I'm trying to use the built-in form functionality this time. The most obviously way to do this seems to be using a ProcessEnginePlugin that configures a custom FormEngine. This worked quite well for providing customized rendering. But then I ran into some roadblocks.

      Since form rendering and submission are each other's dual, I expect these concerns to be located in the same class. In reality, whereas FormEngine is responsible for rendering, it's the FormHandler that's responsible for submission. This is quite awkward for several reasons:

      • the functionality that is common to rendering and submission is needed by these two classes.
      • the FormHandler.submitFormVariables method has no access to the FormData.
      • the only way to provide a custom FormHandler is by putting the camunda:formHandlerClass attribute on every task.

      Sure, I can find ways around these issues, but it quickly becomes quite messy.

      Am I missing something, or is this just the result of organically grown functionality over the years? I would be happy to propose (and implement) some improvements if that's the case.

        This is the controller panel for Smart Panels app

            [CAM-10622] Custom form handling

            Marcus Klimstra created issue -
            Marcus Klimstra made changes -
            Description Original: I've worked with the "Activiti-family" of process engines for over 10 years: from jBPMN to Activiti, Flowable and recently Camunda. So far I've always written my own form handling functionality using custom commands.

            My intention now is to work _with_ instead of _around_ the workflow engine as much as possible, so I'm trying to use the built-in form functionality this time. The most obviously way to do this seems to be using a ProcessEnginePlugin that configures a custom FormEngine. This worked quite well for providing customized rendering. But then I ran into some roadblocks.

            Since form rendering and submission are each other's dual, I expect these concerns to be located in the same class. In reality, whereas FormEngine is responsible for rendering, it's the FormHandler that's responsible for submission. This is quite awkward for several reasons:
            - the functionality that is common to rendering and submission is needed by these two classes.
            - the FormHandler.submitFormVariables method has no access to the FormData.
            - the only way to provide a custom FormHandler is by putting the camunda:formHandlerClass attribute on every task.
            Sure, I can find ways around these issues, but it quickly becomes quite messy.

            Am I missing something, or is this just the result of organically grown functionality over the years? I would be happy to propose (and implement) some improvements if that's the case.
            New: I've worked with the "Activiti-family" of process engines for over 10 years: from jBPMN to Activiti, Flowable and recently Camunda. So far I've always written my own form handling functionality using custom commands.

            My intention now is to work _with_ instead of _around_ the workflow engine as much as possible, so I'm trying to use the built-in form functionality this time. The most obviously way to do this seems to be using a ProcessEnginePlugin that configures a custom FormEngine. This worked quite well for providing customized rendering. But then I ran into some roadblocks.

            Since form rendering and submission are each other's dual, I expect these concerns to be located in the same class. In reality, whereas FormEngine is responsible for rendering, it's the FormHandler that's responsible for submission. This is quite awkward for several reasons:
            - the functionality that is common to rendering and submission is needed by these two classes.
            - the FormHandler.submitFormVariables method has no access to the FormData.
            - the only way to provide a custom FormHandler is by putting the camunda:formHandlerClass attribute on every task.

            Sure, I can find ways around these issues, but it quickly becomes quite messy.

            Am I missing something, or is this just the result of organically grown functionality over the years? I would be happy to propose (and implement) some improvements if that's the case.
            Yana Vasileva made changes -
            Assignee New: Yana Vasileva [ yana.vasileva ]

            Hi marcusk,

            Thank you for reaching out to with this.

            As a first step, I would like to understand better your goal and the reasoning behind the feature request (before the decision for custom implementation of custom FormEngine).
            You mentioned that you want to use the "built-in form functionality", could you tell us more about what have you tried and what is missing for you?
            Have you looked into the Embedded Task Forms functionality (User guide)? Does it fit your needs?
            After giving us more details on the above, I will continue with your concrete questions/bullets.

            Best regards,
            Yana

            Yana Vasileva added a comment - Hi marcusk , Thank you for reaching out to with this. As a first step, I would like to understand better your goal and the reasoning behind the feature request (before the decision for custom implementation of custom FormEngine). You mentioned that you want to use the "built-in form functionality", could you tell us more about what have you tried and what is missing for you? Have you looked into the Embedded Task Forms functionality ( User guide )? Does it fit your needs? After giving us more details on the above, I will continue with your concrete questions/bullets. Best regards, Yana

            Hi Yana,

            With "built-in form functionality" I meant the API for rendering forms (FormService::getRenderedTaskForm, which delegates to FormEngine for the actual rendering) and submitting forms (FormService::submitTaskForm, which delegates to FormHandler for converting the submitted values to process variables), and the API for customizing their behavior (by configuring a different FormEngine using processEngineConfiguration::setCustomFormEngines / by providing a custom FormHandler in camunda:formHandlerClass).

            The built-in task form renderers (including Embedded Task Forms and Generated Task Forms) and default submit-handling do not fit my needs. To be more concrete, the way we do form handling is by providing a form class that contains (potentially nested) properties for every form field, and annotations containing meta-data about how a field should be rendered, validated etc. When a form is submitted the entered values are put into a new instance of the form class, which is then stored in a single process variable.

            So just to be clear, Camunda already supports the configuration of a custom FormEngine, ánd providing custom FormHandlers. It's just that I've encountered some issues when actually trying to use these extension points. For this I have some improvements in mind.

            Regards, Marcus

            Marcus Klimstra added a comment - Hi Yana, With "built-in form functionality" I meant the API for rendering forms (FormService::getRenderedTaskForm, which delegates to FormEngine for the actual rendering) and submitting forms (FormService::submitTaskForm, which delegates to FormHandler for converting the submitted values to process variables), and the API for customizing their behavior (by configuring a different FormEngine using processEngineConfiguration::setCustomFormEngines / by providing a custom FormHandler in camunda:formHandlerClass). The built-in task form renderers (including Embedded Task Forms and Generated Task Forms) and default submit-handling do not fit my needs. To be more concrete, the way we do form handling is by providing a form class that contains (potentially nested) properties for every form field, and annotations containing meta-data about how a field should be rendered, validated etc. When a form is submitted the entered values are put into a new instance of the form class, which is then stored in a single process variable. So just to be clear, Camunda already supports the configuration of a custom FormEngine, ánd providing custom FormHandlers. It's just that I've encountered some issues when actually trying to use these extension points. For this I have some improvements in mind. Regards, Marcus
            Yana Vasileva made changes -
            Link New: This issue is related to CAMTEAM-31 [ CAMTEAM-31 ]

            Hi Marcus,

            Thank you for your explanation. I have a couple of more questions:

            1. the functionality that is common to rendering and submission is needed by these two classes.
              > FormEngine and FormHandler are interfaces, maybe you can implement one class which implement both of them?
            2. the FormHandler.submitFormVariables method has no access to the FormData.
              > It will be interesting to give us an example of your use case why do you need access to the FormData.
            3. the only way to provide a custom FormHandler is by putting the camunda:formHandlerClass attribute on every task.
              > Did you consider implementing a ParseListener?

            Yana Vasileva added a comment - Hi Marcus, Thank you for your explanation. I have a couple of more questions: the functionality that is common to rendering and submission is needed by these two classes. > FormEngine and FormHandler are interfaces, maybe you can implement one class which implement both of them? the FormHandler.submitFormVariables method has no access to the FormData. > It will be interesting to give us an example of your use case why do you need access to the FormData . the only way to provide a custom FormHandler is by putting the camunda:formHandlerClass attribute on every task. > Did you consider implementing a ParseListener?

            Marcus Klimstra added a comment - - edited

            Hi Yana,

            I've created a minimized version of our current implementation here, including the use of a ParseListener as you suggested. I'm relatively happy with this implementation, except for the breaking issue I mention in point 3.

            2. It will be interesting to give us an example of your use case why do you need access to the FormData.

            Remember that in our case the formKey contains the fully qualified name of the class that represents the form, which together with the deploymentId is used to load the class. This class is just as much needed for form submission as it is for rendering, because during submitForm a new instance of that class is created, and its fields are filled using the entered values. As a workaround I retrieve the formKey and deploymentId in parseConfiguration, so they can be used by submitFormVariables, which delegates most work to the FormEngine (where I think it should be), and then stores the resulting Form-instance in a process variable.

            1. FormEngine and FormHandler are interfaces, maybe you can implement one class which implement both of them?

            That's possible, but each task definition has its own FormHandler instance (since it's also responsible for storing the result of parseConfiguration), so you'd still need additional measures to be able to share state (e.g. caching, so we don't have to load en analyse the form class each time) between the form engine and the form handlers. The solution I chose is to have a reference to the FormEngine in each FormHandler.

            3. Did you consider implementing a ParseListener?

            Good idea, I did not think of that I used it in the linked implementation, and it works quite well in principle.

            However, this has brought to light a fundamental issue with overring a FormHandler in a ParseListener. When ParseListener#parseUserTask is called, a FormHandler has already been assigned to the task (by BpmnParse::parseTaskDefinition), and its parseConfiguration-method has already been invoked (from the same place). Therefore parseConfiguration on the new FormHandler will never be called!

            Now I don't want to come across as negative, so let me offer some suggestions instead of complaining

            In the short term, a small enhancement would solve my primary issue: support the possibility to specify a default FormHandler-factory* in the ProcessEngineConfiguration. This default is then used (instead of "new DefaultTaskFormHandler()" as it is now) to assign a form handler when it isn't explicitly specified using camunda:formHandlerClass. This change would remove the need for a ParseListener to set the custom FormHandler, and therefore circumvent the problem I mentioned in point 3.
            (* why a factory instead of a class? so we can easily pass additional constructor parameters)

            On the longer term, if Camunda ever considers a redesign of the form handling API, I have the following ideas about that:

            • FormHandler should only be responsible for parsing the task definition XML, and create FormData-instances based on that. It would also make sense to have it decide on the name of the FormEngine to use (e.g. based on the parsed XML, or bound to the FormHandler type), unless one is specified explicitly.
            • FormEngine should be responsible for form rendering and submission.
            • Perhaps have FormEngine parameterized on the subtype of (Start/Task)FormData, so FormHandler can easily pass additional data to the FormEngine it chose.

            I think this would be a cleaner seperation of concerns, making the functionality easier to understand and extend.

            Marcus Klimstra added a comment - - edited Hi Yana, I've created a minimized version of our current implementation here , including the use of a ParseListener as you suggested. I'm relatively happy with this implementation, except for the breaking issue I mention in point 3. 2. It will be interesting to give us an example of your use case why do you need access to the FormData. Remember that in our case the formKey contains the fully qualified name of the class that represents the form, which together with the deploymentId is used to load the class . This class is just as much needed for form submission as it is for rendering, because during submitForm a new instance of that class is created, and its fields are filled using the entered values. As a workaround I retrieve the formKey and deploymentId in parseConfiguration , so they can be used by submitFormVariables , which delegates most work to the FormEngine (where I think it should be), and then stores the resulting Form-instance in a process variable. 1. FormEngine and FormHandler are interfaces, maybe you can implement one class which implement both of them? That's possible, but each task definition has its own FormHandler instance (since it's also responsible for storing the result of parseConfiguration ), so you'd still need additional measures to be able to share state (e.g. caching, so we don't have to load en analyse the form class each time) between the form engine and the form handlers. The solution I chose is to have a reference to the FormEngine in each FormHandler. 3. Did you consider implementing a ParseListener? Good idea, I did not think of that I used it in the linked implementation, and it works quite well in principle. However, this has brought to light a fundamental issue with overring a FormHandler in a ParseListener. When ParseListener#parseUserTask is called, a FormHandler has already been assigned to the task (by BpmnParse::parseTaskDefinition), and its parseConfiguration-method has already been invoked (from the same place). Therefore parseConfiguration on the new FormHandler will never be called! Now I don't want to come across as negative, so let me offer some suggestions instead of complaining In the short term, a small enhancement would solve my primary issue: support the possibility to specify a default FormHandler-factory* in the ProcessEngineConfiguration. This default is then used (instead of "new DefaultTaskFormHandler()" as it is now) to assign a form handler when it isn't explicitly specified using camunda:formHandlerClass. This change would remove the need for a ParseListener to set the custom FormHandler, and therefore circumvent the problem I mentioned in point 3. (* why a factory instead of a class? so we can easily pass additional constructor parameters) On the longer term, if Camunda ever considers a redesign of the form handling API, I have the following ideas about that: FormHandler should only be responsible for parsing the task definition XML, and create FormData-instances based on that. It would also make sense to have it decide on the name of the FormEngine to use (e.g. based on the parsed XML, or bound to the FormHandler type), unless one is specified explicitly. FormEngine should be responsible for form rendering and submission. Perhaps have FormEngine parameterized on the subtype of (Start/Task)FormData, so FormHandler can easily pass additional data to the FormEngine it chose. I think this would be a cleaner seperation of concerns, making the functionality easier to understand and extend.

            Marcus Klimstra added a comment - - edited

            I said:

            In the short term, a small enhancement would solve my primary issue: support the possibility to specify a default FormHandler-factory* in the ProcessEngineConfiguration.

            but while working on a PR for this, this turned out have more impact than I initially thought, because there's no clear access path from BpmnParse to the ProcessEngineConfiguration.

            So I'm now working on the bug concerning the faulty interaction between ParseListeners and FormHandlers instead, which I probably should have in the first place That should solve my primary issue.

            Marcus Klimstra added a comment - - edited I said: In the short term, a small enhancement would solve my primary issue: support the possibility to specify a default FormHandler-factory* in the ProcessEngineConfiguration. but while working on a PR for this, this turned out have more impact than I initially thought, because there's no clear access path from BpmnParse to the ProcessEngineConfiguration. So I'm now working on the bug concerning the faulty interaction between ParseListeners and FormHandlers instead, which I probably should have in the first place That should solve my primary issue.

            Yana Vasileva added a comment - - edited

            Hi Marcus,

            Thank you for the provided input. I just want to verify that I understood you correctly.
            You decided to choose the approach to implement a ParseListener. However, you stumble upon the problem that FormHandler has already been assigned to the task when the ParseListener#parseUserTask is called.
            And now you are focused on resolving this problem, is that right?

            Yana Vasileva added a comment - - edited Hi Marcus, Thank you for the provided input. I just want to verify that I understood you correctly. You decided to choose the approach to implement a ParseListener. However, you stumble upon the problem that FormHandler has already been assigned to the task when the ParseListener#parseUserTask is called. And now you are focused on resolving this problem, is that right?

              Unassigned Unassigned
              marcusk Marcus Klimstra
              Votes:
              0 Vote for this issue
              Watchers:
              4 Start watching this issue

                Created:
                Updated:
                Resolved: