Applies to Infiniti v8.1 or later.
 

Introduction

 
Infiniti supports the majority of eForm inputs, but occasionally a unique user interface or control, specific to an implementation is required. In these cases, a Custom Question Provider may be developed.
 
A Custom Question Provider (generally referred to as a Custom Question) allows organizations to develop their own question types that can be used where needed throughout their Infiniti Projects. A custom question may define a user interface to capture information or may be used for data manipulation.

Custom Question Provider Development Walkthrough

Custom Question Providers can be developed in house or by a third party to handle custom or unique situations when necessary. Custom Questions are developed as a module that is deployed to an existing Infiniti environment.
 
The walkthroughs below have been created using Microsoft’s Visual Studio 2013. All sample code is in C# based on version 4.5.1 of the .NET Framework.

How to Develop a ‘Hello World’ Custom Question Provider

1. Open Visual Studio and create a new Class Library Project and give it a meaningful name. For this example, we will use ‘HelloWorldQuestion’. Ensure that the .NET Framework 4.5.1 is selected.
 
 
2. Rename Class1 to something more meaningful such as ‘HelloWorldQuestion’.
 
 
3. Click ‘Yes’ to the rename all references prompt.
 
 
4. Add the following references to the project
 
.NET References:
  • System.Configuration
Infiniti References (usually located C:\inetpub\wwwroot\Infiniti\Produce\bin)
  • Intelledox.CustomQuestion.dll
  • Intelledox.InfinitiProviderBase.dll
  • Intelledox.QAWizard.dll
  • Intelledox.Model.dll
  • Intelledox.Common.dll
 
 
5. Inherit the Intelledox.CustomQuestion.CustomQuestionProvider and override necessary CustomQuestionProvider methods, as per the sample below.
 
using System;

namespace HelloWorldQuestion
{
    public class HelloWorldQuestion : Intelledox.CustomQuestion.CustomQuestionProvider
    {
        public override Guid CustomQuestionTypeID()
        {
            throw new NotImplementedException();
        }

        public override void UpdateAttributes(string controlPrefix, System.Collections.Specialized.NameValueCollection form, Intelledox.QAWizard.CustomQuestionProperties props)
        {
        }

        public override void WriteQuestionHtml(string controlPrefix, Intelledox.QAWizard.CustomQuestionProperties props, System.IO.StreamWriter writer)
        {
            throw new NotImplementedException();
        }
    }
}
 
The project should build at this point without error.
 
6. Code the CustomQuestionTypeID() method so that it returns a GUID to identify the Custom Question.
 
public override Guid CustomQuestionTypeID()
{
    return new Guid("3117CAEA-52A1-475A-B6D6-A7CC96B40326");
}
 
7. While it is possible to create Custom Questions that don’t display anything on the screen, in most cases you will want to display something. For this simple example, we’ll just display the words “Hello world” on the screen. You do this by defining the html that should display in the form in the play where the question is. Fill this in the WriteQuestionHtml function by writing to the StreamWriter that is passed into the function.
 
In this case, we’ll just have our question write an input with the default value of “Hello World” on the screen.
 
public override bool HasBeenAnswered(Intelledox.QAWizard.CustomQuestionProperties props)
{
    if (props.ContainsAttribute(RETURNTEXTGUID))
    {
        return !String.IsNullOrEmpty(props.GetAttributeString(RETURNTEXTGUID));
    }
    else
    {
        return false;
    }
}
 
Note: the use of the “controlPrefix” parameter. It is important to use this prefix when defining fields from which the values are important. At this point, the value of the field isn’t useful, but it’s a good habit to always use the controlPrefix.
 
The controlPrefix will uniquely identify a particular instance of a Custom Question. This is needed when multiple questions of the same type might be on the screen at once (such as if your Custom Question is in a repeated Section).
 
Here we have appended “_myInput” to the end of the controlPrefix.
 
8. Add a constructor method to the class and call the ‘RegisterCustomQuestionType()’ method to register the Custom Question after it has been deployed.
 
Note: that the third parameter to the RegisterCustomQuestionType function is for an icon that will appear in Design. In this example, we’ll leave it blank – meaning no icon will appear next to this question in design.
 
public HelloWorldQuestion()
{
    RegisterCustomQuestionType(CustomQuestionTypeID(), "Hello World", null);
}
 
9. Build your Custom Question to ensure it compiles without error.
 
The Question can now be deployed and tested.

Deploying a Custom Question

Custom Questions are deployed to an Infiniti environment by copying the Custom Question Provider .dll file to the Produce bin directory and referencing it within Produce’s web.config file.
 
10. Locate your ‘HelloWorldQuestion.dll’ file and copy it to the Produce bin directory (usually located C:\inetpub\wwwroot\Infiniti\Produce\bin).
 
11. Open the produce web.config file and locate the section of the file. It may contain references to other Custom Questions already.
 
12. Add a new Custom Question provider element using the following syntax to the web.config
<add name="name" type="namespace.class, AssemblyName">
<customquestion>
  <providers>
    <add name="Hello World" type="HelloWorldQuestion.HelloWorldQuestion,
        HelloWorldQuestion"/>
  </add>
</providers>
</customquestion>
</add>
 
13. Save the web.config file.
 
14. Navigate to Produce in your browser, an absence of error messages suggests the Custom Question has installed correctly.
 
15. Open a project in Infiniti Design and note that the question has been added to the question toolbox.
 
 
 
The Custom Question can now be added to the question set just like any other question type. When you run a project in Produce with the Question you should see a field with the initial value of “Hello World” printed on the screen.

Debugging Custom Questions

After deploying a Custom Question it can be debugged by attaching Visual Studio to the w3wp.exe process and running the project in Produce.
 

Custom Question Inputs, Outputs, Local Variables, Attributes and Runtime Behavior.

Like the existing packaged question types, a custom question will have its own inputs, outputs and variables. For custom questions these need to be handled in a particular manner.
At runtime only one instance of the custom question provider object is created, even when the custom question appears multiple times throughout the eForm. Thus, non-static global variables must never be used as they become shared and subsequently interfere with each other.
 
For the above, an attribute collection contained on the CustomQuestionProperties object is available and is passed into most functions. The individual attribute values can be set and updated using the following functions:
 
  • ContainsAttribute(Guid) – returns whether or not the attribute is contained in the collection.
  • GetAttribute(Guid) – returns the value of the attribute as an object.
  • GetAttributeBool(Guid) – returns the value of the attribute as a boolean.
  • GetAttributeString(Guid) – returns the value of the attribute as a string.
  • UpdateAttribute(Guid, Object) – Sets the value of the attribute. If the attribute id doesn’t exist in the collection yet, it is created.
 
All attributes are identified by a Guid.
 
The sections below explain how to receive question inputs define them as attributes, maintain their values and finally output them as content for a document or action.

Using Inputs

Like the currently packaged Infiniti question types require their own unique inputs most Custom Questions will need a collection of defined inputs that will affect their behavior. For example, a default value input might pre-populate a text box or similar control.
 
Some common question inputs are already part of the custom question and are present without the need for custom development including:
  • Question Text
  • Text Position
  • Mandatory
  • Help Text
  • Conditions also work just as any other question.
Following on from the example above, the code below uses a question input to display some text within the question UI.
 
16. Inputs are identified by a GUID, it is best practice to declare this GUID as a global variable.
 
public class HelloWorldCustomQuestion : Intelledox.CustomQuestion.CustomQuestionProvider
{
  Guid DEFAULTVALUEGUID = 
     new Guid("56D4DBEB-5B6A-468A-B78D-0CBFDBEC1E1E");
  …
  …
}
 
17. Register the Input in the constructor function
 
public class HelloWorldQuestion : Intelledox.CustomQuestion.CustomQuestionProvider
{
    private Guid DEFAULTVALUEGUID = new Guid("56D4DBEB-5B6A-468A-B78D-0CBFDBEC1E1E");

    public HelloWorldQuestion()
    {
        RegisterCustomQuestionType(CustomQuestionTypeID(), "Hello World", null);
        RegisterCustomQuestionInput(DEFAULTVALUEGUID, "Default Value", 1, true);
    } 
18. If you build your project now and deploy it (remember to navigate to Produce and also to reload Design), the input value can now be specified in Design.
 
 
 
19. The input values can be retrieved in most of the question’s runtime functions from the CustomQuestionProperties object that is passed in. The inputs are a collection located at CustomQuestionProperties.Question.Inputs.
 
A good practice is to read a question’s inputs in the InitialiseInputs function and set their values to the Attributes collection. The InitialiseInputs function is called once at runtime, when the question is first loaded up into the form. So this is a good place to set the attributes of the question so that its initial state is known and controlled.
 
public override void InitialiseInputs(Intelledox.QAWizard.CustomQuestionProperties props)
{
    foreach (Intelledox.QAWizard.CustomQuestionInput input in props.Question.Inputs)
    {
        if (input.InputTypeId == DEFAULTVALUEGUID)
        {
            props.UpdateAttribute(DEFAULTVALUEGUID, input.Value.ToString());
        }
    }
} 
 
While this has been included as an example, it’s not actually necessary to override the InitialiseInputs function. The default implementation of this in the base CustomQuestionProvider class will do this for you. This is that implementation:
 
public virtual void InitialiseInputs(QAWizard.CustomQuestionProperties props)
{
    foreach (QAWizard.CustomQuestionInput input in props.Question.Inputs)
    {
        props.UpdateAttribute(input.InputTypeId, input.Value);
    }
}
 
Usually the Initialise Inputs function will only be overwritten if there’s extra logic needed.
 
20. As the attribute for the Display Text has been set, it can now be retrieved in other functions. Let’s use it to update the WriteQuestionHtml function.
 
public override void WriteQuestionHtml(string controlPrefix, Intelledox.QAWizard.CustomQuestionProperties props, System.IO.StreamWriter writer)
{
    writer.Write("<input name=\"" + controlPrefix + "_myInput\" id=\"" + controlPrefix + "_myInput\" value=\"" + props.GetAttributeString(DEFAULTVALUEGUID) + "\"/>");
}
 
21. If you deploy and run a project now with the Hello World Question, you will see that whatever was defined for the input will be displayed.
 

Using Custom Question Outputs

Most Custom Questions will want to return a value or values to Infiniti. Using a simple input field as an example, most of the time it would be desirable to use whatever had been input into that field elsewhere in the project or generated document.
 
1 Similar to Custom Question Inputs, Outputs must be declared and registered.
 
public class HelloWorldQuestion : Intelledox.CustomQuestion.CustomQuestionProvider
{
    private Guid DEFAULTVALUEGUID = new Guid("56D4DBEB-5B6A-468A-B78D-0CBFDBEC1E1E");
    private Guid RETURNTEXTGUID = new Guid("96BE5F95-7BF7-4250-B01B-D85ED9AD5A14");

    public HelloWorldQuestion()
    {
        RegisterCustomQuestionType(CustomQuestionTypeID(), "Hello World", null);
        RegisterCustomQuestionInput(DEFAULTVALUEGUID, "Default Value", 1, true);
        RegisterCustomQuestionOutput(RETURNTEXTGUID, "Return Text", Intelledox.Model.CustomQuestionOutputType.Text);
    }
	
22. In order for Infiniti to find the output when needed, the value of the Output should be set in the attributes collection. This is done in the UpdateAttributes function. The UpdateAttributes function is called whenever a postback occurs in the browser. This is an opportunity for the Custom Question to update its attributes based on what the user has entered.
 
This is also where the controlPrefix comes into play. A NameValueCollection is passed into the function with the values of all the controls on the form that started with the controlPrefix – so if don’t give a control the prefix, you’ll never be able to utilize its value.
 
public override void UpdateAttributes(string controlPrefix, System.Collections.Specialized.NameValueCollection form, Intelledox.QAWizard.CustomQuestionProperties props)
{
    props.UpdateAttribute(RETURNTEXTGUID, form[controlPrefix + "_myInput"]);
}
23. Now if you build and deploy your project, and reload Produce/Design; you will find the Output on the Output dropdown on Answers on your Question. This can be referenced just like any other Answer in a project.
 

 

Sanitizing Input

For security, you should always sanitize text you’re displaying that you have no control over. In our case, that’s the Default Value input – we can’t control whether or not malicious html has been inserted in that field.
 
How you sanitize the inputs will depend on where the input value is used. Most likely you will need to sanitize the input for html, or possibly javascript. .NET includes the function System.Net.WebUtility.HtmlEncode which will suit our needs here. 
 
public override void WriteQuestionHtml(string controlPrefix, Intelledox.QAWizard.CustomQuestionProperties props, System.IO.StreamWriter writer)
{
    writer.Write("<input name=\"" + controlPrefix + "_myInput\" id=\"" + controlPrefix + "_myInput\" value=\"" + System.Net.WebUtility.HtmlEncode(props.GetAttributeString(DEFAULTVALUEGUID)) + "\"/>");
}
If you want some html to be allowed, but not all (eg the input comes from a Rich Text Question), you can leverage this function in Infiniti: Intelledox.Common.HtmlParsing.Sanitize. It will leave most standard html formatting in via a whitelist of accepted tags, but will remove anything potentially malicious.
 

Supporting Mandatory

The Mandatory property of a custom question can’t just work out of the box – it has to be communicated to Infiniti whether or not the question has been answered.
Override the HasBeenAnswered function. This function returns true or false as to whether or not the question has been answered. In our case, the question has been answer if Text has been entered.
 
public override bool HasBeenAnswered(Intelledox.QAWizard.CustomQuestionProperties props)
{
    if (props.ContainsAttribute(RETURNTEXTGUID))
    {
        return !String.IsNullOrEmpty(props.GetAttributeString(RETURNTEXTGUID));
    }
    else
    {
        return false;
    }
}

 

Check Validity

This IsValid function can be overwritten to determine whether or not a question is valid. For example, if you only wanted users to enter words starting with “X”, you could put the following code in.
 
public override bool IsValid(Intelledox.QAWizard.CustomQuestionProperties props, ref string validationMessage)
{
    return props.GetAttributeString(RETURNTEXTGUID).StartsWith("X");
}

 

Handling Default Values

1. The question that’s been built so far is a fine example of a custom question type, but there is a flaw with the default value input. You may note in Produce that if you manually input a value then navigate to another page and back again, that the input will revert to the default value state again. This is not a good user experience – if a value is manually input, it should be kept.
 
2. The first step to solve this problem is fairly simple – the reason that the value is being lost is because whenever the question is being displayed, it is using WriteQuestionHtml, which is putting the default value in the field instead of what has been entered.
 
So instead of using the DEFAULTVALUE attribute, we’ll use the RETURNTEXT attribute.
 
public override void WriteQuestionHtml(string controlPrefix, Intelledox.QAWizard.CustomQuestionProperties props, System.IO.StreamWriter writer)
{
    writer.Write("<input controlprefix="" id="\""" name="\" value="\""" />");
}
 
3. The RETURNTEXT attribute still needs to be initialised to the default value though for the first load of the question. This is where overriding the InitialiseInputs function is useful. Instead of the default behavior being to set a DEFAULTVALUE attribute (because DEFAULTVALUE is an input), we’ll set the RETURNTEXT attribute instead. This way RETURNTEXT starts off with the default value.
 
public override void InitialiseInputs(Intelledox.QAWizard.CustomQuestionProperties props)
{
    foreach (Intelledox.QAWizard.CustomQuestionInput input in props.Question.Inputs)
    {
        if (input.InputTypeId == DEFAULTVALUEGUID)
        {
            props.UpdateAttribute(RETURNTEXTGUID, input.Value.ToString());
        }
    }
}
 
4. If you build and deploy the project at this point, we will have solved the problem. If you type in a value, it will be kept even when navigating on and off the page.
 
However, a new problem has been introduced. If the default value is a reference to another question, and you haven’t manually entered anything yet, then the expectation is that if the other question was changed, so too will the default value for our question. That’s not the case though.
 
That’s because we’re only utilizing the DEFAULTVALUE input in the InitiliseInputs function now – which is only called once. If that input changes, then the InputChanged function is called. Currently, we have made no changes to this function, so it is doing its default behavior – which is to update attributes with the same name as the inputs. There’s not much point in updating the DEFAULTVALUE attribute though – we’re using RETURNTEXT in WriteQuestionHtml!
 
Easily fixed: let’s override InputChanged so that when the DEFAULTVALUE changes, we update the RETURNTEXT.
 
public override void WriteQuestionHtml(string controlPrefix, Intelledox.QAWizard.CustomQuestionProperties props, System.IO.StreamWriter writer)
{
    writer.Write("<input name=\"" + controlPrefix + "_myInput\" id=\"" + controlPrefix + "_myInput\" value=\"" + System.Net.WebUtility.HtmlEncode(props.GetAttributeString(RETURNTEXTGUID)) + "\"/>");
    props.UpdateAttribute(PARENTCHANGEFLAGGUID, false);
}
 
5. If you build and deploy now you will actually find that the problem still hasn’t been solved. This is because of the order in which the functions are called. Because our parent question comes earlier in the page, it gets updated first, and so calls InputChanged. Then our question gets updated via the UpdateAttributes function. Finally, everything gets re-drawn in WriteQuestionHtml. If we follow what happens to the RETURNTEXT attribute, we will find the following happens:
  • InputChanged is called. RETURNTEXT attribute is set to the parent’s value, correctly.
  • UpdateAttributes is called. RETURNTEXT attribute is set to whatever was on the screen – this is the old value (and therefore incorrect value)
  • WriteQuestionHtml is called. The wrong value is currently in the RETURNTEXT attribute, so that is what is written to the screen.
 
To fix this problem, we can let the UpdateAttributes function know that it shouldn’t update things, because we’re actually updating because of an Input being changed. To do this, we’ll add a new attribute called PARENTCHANGE FLAG.
 
public class HelloWorldQuestion : Intelledox.CustomQuestion.CustomQuestionProvider
{
    private Guid DEFAULTVALUEGUID = new Guid("56D4DBEB-5B6A-468A-B78D-0CBFDBEC1E1E");
    private Guid RETURNTEXTGUID = new Guid("96BE5F95-7BF7-4250-B01B-D85ED9AD5A14");
    private Guid PARENTCHANGEFLAGGUID = new Guid("DA634B83-F9AA-483E-B157-FEA4BC3349F5");
	
This attribute need not be registered in the constructor, as it’s not an input or an output, so won’t be visible to a user anywhere, it’s just something the question will be using internally.
 
6. Now we will need to update the InitialiseInputs function as we wish to set the default value of this attribute to false.
 
public override void InitialiseInputs(Intelledox.QAWizard.CustomQuestionProperties props)
{
    props.UpdateAttribute(PARENTCHANGEFLAGGUID, false);
    foreach (Intelledox.QAWizard.CustomQuestionInput input in props.Question.Inputs)
    {
        if (input.InputTypeId == DEFAULTVALUEGUID)
        {
            props.UpdateAttribute(RETURNTEXTGUID, input.Value.ToString());
        }
    }
}
 
7. In InputChanged, the flag is set to true.
 
public override void InputChanged(Intelledox.QAWizard.CustomQuestionInput input, object oldValue, Intelledox.QAWizard.CustomQuestionProperties props)
{
    if (input.InputTypeId == DEFAULTVALUEGUID)
    {
        props.UpdateAttribute(RETURNTEXTGUID, input.Value);
        props.UpdateAttribute(PARENTCHANGEFLAGGUID, true);
    }
}
8. In UpdateAttributes, only update if PARENTCHANGEFLAG is false
 
public override void UpdateAttributes(string controlPrefix, System.Collections.Specialized.NameValueCollection form, Intelledox.QAWizard.CustomQuestionProperties props)
{
    if (!(bool)props.GetAttributeBool(PARENTCHANGEFLAGGUID))
    {
        props.UpdateAttribute(RETURNTEXTGUID, form[controlPrefix + "_myInput"]);
    }
}
 
9. Finally, when the question is displayed again, the flag is set back to false.
 
public override void WriteQuestionHtml(string controlPrefix, Intelledox.QAWizard.CustomQuestionProperties props, System.IO.StreamWriter writer)
{
    writer.Write("<input controlprefix="" id="\""" name="\" value="\""" />");
    props.UpdateAttribute(PARENTCHANGEFLAGGUID, false);
}
 
10. Again, if you build and deploy, you will find that this problem has been solved, but the old one re-introduced, in a way. If the user manually inputs something, but then goes back and changes the parent question, the InputChanged event will run and override what the user has manually input.
 
To fix this, we will need to track whether or not the user has made a manual change.
 
First, we’ll add a new attribute called MANUALCHANGEFLAG.
 
public class HelloWorldQuestion : Intelledox.CustomQuestion.CustomQuestionProvider
{
    private Guid DEFAULTVALUEGUID = new Guid("56D4DBEB-5B6A-468A-B78D-0CBFDBEC1E1E");
    private Guid RETURNTEXTGUID = new Guid("96BE5F95-7BF7-4250-B01B-D85ED9AD5A14");
    private Guid PARENTCHANGEFLAGGUID = new Guid("DA634B83-F9AA-483E-B157-FEA4BC3349F5");
    private Guid MANUALCHANGEFLAGGUID = new Guid("5DF485D3-8F80-42E4-968B-489DD3AC7E93");
11. Update the InitialiseInputs function in order to set the default value of this attribute to false.
 
public override void InitialiseInputs(Intelledox.QAWizard.CustomQuestionProperties props)
{
    props.UpdateAttribute(PARENTCHANGEFLAGGUID, false);
    props.UpdateAttribute(MANUALCHANGEFLAGGUID, false);
    foreach (Intelledox.QAWizard.CustomQuestionInput input in props.Question.Inputs)
    {
        if (input.InputTypeId == DEFAULTVALUEGUID)
        {
            props.UpdateAttribute(RETURNTEXTGUID, input.Value.ToString());
        }
    }
}
 
12. In UpdateAttributes, we set the flag to true if the user has manually input something. We can tell if something has been input by seeing if the value has changed.
 
public override void UpdateAttributes(string controlPrefix, System.Collections.Specialized.NameValueCollection form, Intelledox.QAWizard.CustomQuestionProperties props)
{
    if (!(bool)props.GetAttributeBool(PARENTCHANGEFLAGGUID))
    {
        if (props.GetAttributeString(DEFAULTVALUEGUID) != form[controlPrefix + "_myInput"])
        {
            props.UpdateAttribute(MANUALCHANGEFLAGGUID, true);
        }
        props.UpdateAttribute(RETURNTEXTGUID, form[controlPrefix + "_myInput"]);
    }
}
 
13. Finally, in the InputChanged function, we only update the value if the user hasn’t manually changed it yet.
 
public override void InputChanged(Intelledox.QAWizard.CustomQuestionInput input, object oldValue, Intelledox.QAWizard.CustomQuestionProperties props)
{
    if (input.InputTypeId == DEFAULTVALUEGUID)
    {
        if (!(bool)props.GetAttributeBool(MANUALCHANGEFLAGGUID))
        {
            props.UpdateAttribute(RETURNTEXTGUID, input.Value);
            props.UpdateAttribute(PARENTCHANGEFLAGGUID, true);
        }
    }
}

 

References to a Custom Question

References in a project to a Custom Question’s Outputs will work out of the box. Except in the situation where you have a reference on the same page and you want the reference to dynamically update when the user changes the Custom Question. In this case, you will need to initiate a postback so that Infiniti can update the fields on the page.
 
Infiniti provides a TRIGGER_REFRESH string, which provides the javascript required to initiate a refresh. We just have to put it in our html.
 
Of course, we only wish a refresh to occur if it’s actually necessary. Unnecessary refreshes will just provide users with a bad experience.
 
A refresh should occur if:
  • The question has been referenced by something else on the same page. This is flagged by a property that can be accessed at props.Question.IsRealtimeParentQuestion
  • The user has just left the field (the onblur event of an input), and the value in the input has actually been changed.
 
public override void WriteQuestionHtml(string controlPrefix, Intelledox.QAWizard.CustomQuestionProperties props, System.IO.StreamWriter writer)
{
    writer.Write("<input name=\"" + controlPrefix + "_myInput\" id=\"" + controlPrefix + "_myInput\" value=\"" + System.Net.WebUtility.HtmlEncode(props.GetAttributeString(RETURNTEXTGUID)) + "\"");

    if (props.Question.IsRealtimeParentQuestion)
    {
        writer.Write("onblur=\"if ($('#" + controlPrefix + "_myInput_change').val() == '1') {" + TRIGGER_REFRESH + "}\" ");
    }
    writer.Write("/>");

    if (props.Question.IsRealtimeParentQuestion)
    {
        writer.Write("<input type=\"hidden\" id=\"" + controlPrefix + "_myInput_change\" name=\"" + controlPrefix + "_myInput_change\" value=\"0\" />");
        writer.Write("<script type=\"text/javascript\">");
        writer.Write("$('#" + controlPrefix + "_myInput').on('change', function() { $('#" + controlPrefix + "_myInput_change').val('1'); });");
        writer.Write("$('#" + controlPrefix + "_myInput').on('keydown', function(e) ");
        writer.Write("{ ");
        writer.Write("    var code = e.keyCode || e.which;");
        writer.Write("    if (code != '9' && code != '16')");
        writer.Write("        $('#" + controlPrefix + "_myInput_change').val('1'); ");
        writer.Write("});");
        writer.Write("</script>");
    }
}

Reading/Writing from Answer File

Reading and Writing to and from Answer Files is how Infiniti saves the state of a Project in progress – for example, when saving your progress or passing to the next state of a workflow. For Custom Questions, Infiniti will automatically save and read all the attributes of your question for you – ensuring that the next time you load a project the state of the custom question will be the same.
 
However, there will be occasions where you wish to override this functionality. For example, if you are saving sensitive information you may wish to encode it (beyond Infiniti’s standard encoding of Answer Files).
 
This is done by overwriting the FillAnswerFileNode and ReadAnswerFileNode functions, which are the ones used to define the XML nodes that will be written and read to/from the answer files for a custom question.
 
In this case, let’s reduce the size of the Answer File a tiny bit but only saving the RETURNTEXT attribute – it’s the only one that need be recorded.
 
public override void FillAnswerFileNode(System.Xml.Linq.XElement answerFileNode, Intelledox.QAWizard.CustomQuestionProperties props)
{
    if (props.ContainsAttribute(RETURNTEXTGUID))
    {
        answerFileNode.Add(new System.Xml.Linq.XAttribute(RETURNTEXTGUID.ToString(), props.GetAttributeString(RETURNTEXTGUID)));
    }
}

public override void ReadAnswerFileNode(System.Xml.Linq.XElement answerFileNode, Intelledox.QAWizard.CustomQuestionProperties props)
{
    foreach (System.Xml.Linq.XAttribute xmlAttribute in answerFileNode.Attributes())
    {
        if (Intelledox.Common.Extensions.StringExtensions.EqualsIgnoreCase(xmlAttribute.Name.ToString(), RETURNTEXTGUID.ToString()))
        {
            string savedValue = xmlAttribute.Value;
            if (savedValue != props.GetAttributeString(RETURNTEXTGUID))
            {
                props.UpdateAttribute(MANUALCHANGEFLAGGUID, true);
            }
            props.UpdateAttribute(RETURNTEXTGUID, savedValue);
        }
    }
}
 
Note: The MANUALCHANGEFLAG is set to true if the value being read from the answer file doesn’t match the default value – the user has changed this question’s value manually at some point.

 

Code Sample: ‘Hello World’ Custom Question Provider Sample Code

 
using System;

namespace HelloWorldQuestion
{
    public class HelloWorldQuestion : Intelledox.CustomQuestion.CustomQuestionProvider
    {
        private Guid DEFAULTVALUEGUID = new Guid("56D4DBEB-5B6A-468A-B78D-0CBFDBEC1E1E");
        private Guid RETURNTEXTGUID = new Guid("96BE5F95-7BF7-4250-B01B-D85ED9AD5A14");
        private Guid PARENTCHANGEFLAGGUID = new Guid("DA634B83-F9AA-483E-B157-FEA4BC3349F5");
        private Guid MANUALCHANGEFLAGGUID = new Guid("5DF485D3-8F80-42E4-968B-489DD3AC7E93");

        public HelloWorldQuestion()
        {
            RegisterCustomQuestionType(CustomQuestionTypeID(), "Hello World", null);
            RegisterCustomQuestionInput(DEFAULTVALUEGUID, "Default Value", 1, true);
            RegisterCustomQuestionOutput(RETURNTEXTGUID, "Return Text", Intelledox.Model.CustomQuestionOutputType.Text);
        }

        public override Guid CustomQuestionTypeID()
        {
            return new Guid("3117CAEA-52A1-475A-B6D6-A7CC96B40326");
        }

        public override void InitialiseInputs(Intelledox.QAWizard.CustomQuestionProperties props)
        {
            props.UpdateAttribute(PARENTCHANGEFLAGGUID, false);
            props.UpdateAttribute(MANUALCHANGEFLAGGUID, false);
            foreach (Intelledox.QAWizard.CustomQuestionInput input in props.Question.Inputs)
            {
                if (input.InputTypeId == DEFAULTVALUEGUID)
                {
                    props.UpdateAttribute(RETURNTEXTGUID, input.Value.ToString());
                }
            }
        }

        public override void UpdateAttributes(string controlPrefix, System.Collections.Specialized.NameValueCollection form, Intelledox.QAWizard.CustomQuestionProperties props)
        {
            if (!(bool)props.GetAttributeBool(PARENTCHANGEFLAGGUID))
            {
                if (props.GetAttributeString(DEFAULTVALUEGUID) != form[controlPrefix + "_myInput"])
                {
                    props.UpdateAttribute(MANUALCHANGEFLAGGUID, true);
                }
                props.UpdateAttribute(RETURNTEXTGUID, form[controlPrefix + "_myInput"]);
            }
        }

        public override void WriteQuestionHtml(string controlPrefix, Intelledox.QAWizard.CustomQuestionProperties props, System.IO.StreamWriter writer)
        {
            writer.Write("<input name=\"" + controlPrefix + "_myInput\" id=\"" + controlPrefix + "_myInput\" value=\"" + System.Net.WebUtility.HtmlEncode(props.GetAttributeString(RETURNTEXTGUID)) + "\"");

            if (props.Question.IsRealtimeParentQuestion)
            {
                writer.Write("onblur=\"if ($('#" + controlPrefix + "_myInput_change').val() == '1') {" + TRIGGER_REFRESH + "}\" ");
            }
            writer.Write("/>");

            if (props.Question.IsRealtimeParentQuestion)
            {
                writer.Write("<input type=\"hidden\" id=\"" + controlPrefix + "_myInput_change\" name=\"" + controlPrefix + "_myInput_change\" value=\"0\" />");
                writer.Write("<script type=\"text/javascript\">");
                writer.Write("$('#" + controlPrefix + "_myInput').on('change', function() { $('#" + controlPrefix + "_myInput_change').val('1'); });");
                writer.Write("$('#" + controlPrefix + "_myInput').on('keydown', function(e) ");
                writer.Write("{ ");
                writer.Write("    var code = e.keyCode || e.which;");
                writer.Write("    if (code != '9' && code != '16')");
                writer.Write("        $('#" + controlPrefix + "_myInput_change').val('1'); ");
                writer.Write("});");
                writer.Write("</script>");
            }
            props.UpdateAttribute(PARENTCHANGEFLAGGUID, false);
        }

        public override bool HasBeenAnswered(Intelledox.QAWizard.CustomQuestionProperties props)
        {
            if (props.ContainsAttribute(RETURNTEXTGUID))
            {
                return !String.IsNullOrEmpty(props.GetAttributeString(RETURNTEXTGUID));
            }
            else
            {
                return false;
            }
        }

        public override void InputChanged(Intelledox.QAWizard.CustomQuestionInput input, object oldValue, Intelledox.QAWizard.CustomQuestionProperties props)
        {
            if (input.InputTypeId == DEFAULTVALUEGUID)
            {
                if (!(bool)props.GetAttributeBool(MANUALCHANGEFLAGGUID))
                {
                    props.UpdateAttribute(RETURNTEXTGUID, input.Value);
                    props.UpdateAttribute(PARENTCHANGEFLAGGUID, true);
                }
            }
        }

        public override void FillAnswerFileNode(System.Xml.Linq.XElement answerFileNode, Intelledox.QAWizard.CustomQuestionProperties props)
        {
            if (props.ContainsAttribute(RETURNTEXTGUID))
            {
                answerFileNode.Add(new System.Xml.Linq.XAttribute(RETURNTEXTGUID.ToString(), props.GetAttributeString(RETURNTEXTGUID)));
            }
        }

        public override void ReadAnswerFileNode(System.Xml.Linq.XElement answerFileNode, Intelledox.QAWizard.CustomQuestionProperties props)
        {
            foreach (System.Xml.Linq.XAttribute xmlAttribute in answerFileNode.Attributes())
            {
                if (Intelledox.Common.Extensions.StringExtensions.EqualsIgnoreCase(xmlAttribute.Name.ToString(), RETURNTEXTGUID.ToString()))
                {
                    string savedValue = xmlAttribute.Value;
                    if (savedValue != props.GetAttributeString(RETURNTEXTGUID))
                    {
                        props.UpdateAttribute(MANUALCHANGEFLAGGUID, true);
                    }
                    props.UpdateAttribute(RETURNTEXTGUID, savedValue);
                }
            }
        }

    }
}

 

Defining an Image for the Question Type

An image can also be defined for custom question. It is the last parameter when registering a question type. That's done via the following code in the Password Custom Question:
 
            RegisterCustomQuestionType(CustomQuestionTypeID(), "Password", GetIcon());

        private Byte[] GetIcon()
        {
            byte[] buffer = null;
            Stream resourceStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("HelloWorldQuestion.question_Password.png");
            try
            {
                buffer = new byte[Convert.ToInt32(resourceStream.Length) + 1];
                resourceStream.Read(buffer, 0, Convert.ToInt32(resourceStream.Length));
            }
            catch (Exception)
            {
            }
            finally
            {
                resourceStream.Close();
            }


            return buffer;
        }
 
Where question_Password.png is an embedded resource.
 

Related Articles

Keywords

questions