Help
Example

This example uses the Web Services API to display and score a custom question. It uses variables and the question body to demonstrate how to use these values in a question. It also allows for save and continue, by displaying the previous answers in Active mode. The example consists of a basic chess question and is written in C# using ASP.NET MVC, however you can use any language and web server that can return XML.

Define Question

First, create a question of type Custom. Point the question to your web service. Also define the variables for this question and the text to display.
Note that your custom question can choose whether to display the body text of the question. In either case, fill out the body, as it shows in summaries and when searching for questions.

Type: Custom
Custom: http://mywebserver/chess/question
Var: row = 0..7
Var: col = 0..7
Var: figure = queen,rook,bishop,knight
1) Indicate all valid locations that this piece can move to
Render Question

The web service to render the question return an XML document described in <custom>. In this example we use the XDocument helper class to generate valid XML.

public string Question()
{
    XDocument request = XDocument.Load(Request.InputStream);

    string id = request.Root.Attribute("id").Value;
    string mode = request.Root.Attribute("mode").Value;

    XDocument custom = null;

    switch (mode)
    {
        case "None":
            custom =
            new XDocument(
                new XElement("custom",
                    new XElement("version", "1"),
                    // Render it in Active and Review mode, don't worry about Print, also provide a template
                    new XElement("display", "Active,Review,Template")
                )
            );
            break;
        case "Template":
            custom =
            new XDocument(
                new XElement("custom",
                    new XElement("version", "1"),
                    new XElement("body",
                        new XAttribute("mode","Template"),
                        "<question schema=\"2\"><body>Indicate all valid locations that this piece can move to</body><interaction type=\"custom\" /><parameters><parameter name=\"row\" type=\"Range\" min=\"0\" max=\"7\" step=\"1\" /><parameter name=\"col\" type=\"Range\" min=\"0\" max=\"7\" step=\"1\" /><parameter name=\"figure\" type=\"List\"><values><value>queen</value><value>rook</value><value>bishop</value><value>knight</value></values></parameter></parameters></question>")
                )
            );
            break;
        case "Active":
        case "Review":
            custom =
            new XDocument(
                new XElement("custom",
                    new XElement("version", "1"),
                    // Grade this question on the server
                    new XElement("score", "http://localhost:1606/Chess/Score"),
                    // Render it in Active and Review mode, don't worry about Print
                    new XElement("privatedata", "Private Data"),
                    new XElement("publicdata", "'Public' Data"),
                    new XElement("body", GetBody(request.Root))
                )
            );
            break;
    }
    return custom.ToString();
}

Most of the work is done in the method GetBody. The above method simply aggregates all the information. The version element allows the web service to keep multiple version of the question while maintaining backwards compatibility. The path to the web service to score the question is important, without it BrainHoney routes the question to the instructor for manual grading. For this question we also indicate that this server handles the Active and Review modes. GetBody returns the HTML (including Javascript) that contains the UI for the question.

private string GetBody(XElement info)
{
    // Get the information we need from the request
    string id = info.Attribute("id").Value;
    bool active = info.Attribute("mode").Value == "Active";

    string header = info.Element("question").Element("body").Value;
    string row = info.XPathSelectElement("submission/attemptquestion/parameters/parameter[@name='row']").Value;
    string col = info.XPathSelectElement("submission/attemptquestion/parameters/parameter[@name='col']").Value;
    int figure = GetFigure(info.XPathSelectElement("submission/attemptquestion/parameters/parameter[@name='figure']").Value);
    string selection = info.Element("submission").Element("answer").Value;

    // Take the template and substitude our variable items
    return Regex.Replace(Template, @"\$[a-z]+\$", m =>
        {
            switch (m.Value)
            {
                case "$row$": return row;
                case "$col$": return col;
                case "$active$": return XmlConvert.ToString(active);
                case "$id$": return id;
                case "$figure$": return XmlConvert.ToString(figure);
                case "$selection$": return selection;
                case "$header$": return header;
            }

            return m.Value;
        });
}

GetBody uses a template and substitudes some values received from the request, including random parameters and the question id. The details of the UI template changes for each custom question. The listing for the example is below. The one common required feature of all questions is a call to CQ.setAnswer with the passed in question id and the student answer. Review the table below for a brief description of the replacement values.

NameDescription
idUnique identifier used with the Javascript API, including CQ.setAnswer. You should also use this to make all HTML elements unique, so that you can have two or more of your custom questions on the same assessment.
activeSet to true if the mode is active, that is the student is currently taking the exam. False for review mode.
headerThis is the body of the question that this custom question uses as the header displayed above the board.
row, col, figureThese are variable parameters the question uses to randomize the question.
selectionThe current student answer as set by CQ.setAnswer. Note that Active mode question may contain an answer, if the student clicked save and then continued. You should therefore always render the answer, even in Active mode. The advanced editor implementation of CQ.setAnswer calls the browser alert with the answer to allow you easy troubleshooting of your custom question.

After the web service returns the information, BrainHoney can render the custom question:

Score Question

After the student submits the exam, BrainHoney calls the score URL specified above to automatically score the quiz. The data sent to this web service is indentical to the call above. The simple custom response requires just the id and points possible (both passed in with the request) and the points computed. For the rare case where computer grading the particular answer is impossible, BrainHoney allows the custom scoring to leave out the pointscomputed value, which sends this question to the teacher for grading.

public string Score()
{
    XDocument request = XDocument.Load(Request.InputStream);

    string partId = request.Root.Element("submission").Attribute("partid").Value;
    double possible = XmlConvert.ToDouble(request.XPathSelectElement("info/response").Attribute("pointspossible").Value);

    double computed = GetScore(request.Root,possible);

    XDocument custom =
        new XDocument(
            new XElement("custom",
                new XElement("response",
                    new XAttribute("type", "submission"),
                    new XAttribute("foreignid", partId),
                    new XAttribute("pointspossible", XmlConvert.ToString(possible)),
                    new XAttribute("pointscomputed", XmlConvert.ToString(computed))
                )
            )
        );

    return custom.ToString();
}

When computing the score never assume anything about the points possible, always scale your score against the passed in value.

private double GetScore(XElement info, double possible)
{
    string answer = info.Element("submission").Element("answer").Value;
    // Perform our custum grading
		double rawScore = .8;
		
		// Always scale the points to possible
    return rawScore * possible;
}

Details

The template for the custom question contains mainly javascript. The various variable, most importantly $id$, will be replaced in GetBody. Notice that in this example we call CQ.setAnswer whenever the answer changes. You can also register a callback with CQ.onBeforeSave to set the answer only when the student is about to submit (or save) the assessment.

const string Template = @"
$header$<br />
<canvas class='chess' id='board$id$' width='400' height='400'>
    This question requires a browser that can display the HTML5 canvas element, such as Firefox.
</canvas>
<style type='text/css'>  
    canvas.chess { border: 2px solid black; }  
</style>  
<script type='text/javascript'>
var canvas = document.getElementById('board$id$');
if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    var data =
    {
        original: { col: $col$, row: $row$ },
        figure: $figure$,
        selection: [$selection$],
        active: $active$
    };

    canvas.data = data;

    canvas.drawSelected = function(ctx, cell) {
        var x = cell.col * 50;
        var y = cell.row * 50;
        var radgrad = ctx.createRadialGradient(x+25, y+25, 0, x+25, y+25, 20);
        radgrad.addColorStop(0, 'yellow');
        radgrad.addColorStop(0.9, 'orange');
        radgrad.addColorStop(1, 'rgba(255,0,0,0)');
        ctx.fillStyle = radgrad;
        ctx.fillRect(x, y, 50, 50);
    }

    canvas.clearSelected = function(ctx, cell) {
        var color = (cell.row + cell.col) % 2;
        if (color == 0)
            ctx.fillStyle = 'rgb(255,255,255)';
        else
            ctx.fillStyle = 'rgb(140,140,140)';
        var x = cell.col * 50;
        var y = cell.row * 50;
        ctx.fillRect(x, y, 50, 50);
    }

    canvas.isSelected = function(cell) {
        for (var i = 0; i < this.data.selection.length; i++)
            if (this.data.selection[i].row == cell.row && this.data.selection[i].col == cell.col)
                return true;
        return false;
    }
    canvas.removeCell = function(cell) {
        for (var i = 0; i < this.data.selection.length; i++)
            if (this.data.selection[i].row == cell.row && this.data.selection[i].col == cell.col) {
                this.data.selection.splice(i, 1);
            }
    }
    canvas.addCell = function(cell) {
        this.data.selection.push(cell);
    }
    canvas.getCursorPosition = function (e) {
        var x = e.pageX-this.offsetLeft;
        var y = e.pageY-this.offsetTop;
        x -= this.getParentLeft(this.offsetParent);
        y -= this.getParentTop(this.offsetParent);
        var cell = { row: Math.floor(y / 50), col: Math.floor(x / 50) };
        return cell;
    }
    canvas.getParentLeft = function (elem) {
        if (elem) {
            return elem.offsetLeft + this.getParentLeft(elem.offsetParent);
        }
        return 0;
    }
    canvas.getParentTop = function (elem) {
        if (elem) {
            return elem.offsetTop + this.getParentTop(elem.offsetParent);
        }
        return 0;
    }
    canvas.chessCanvasOnClick = function(e) {
        canvas = e.target;

        var cell = canvas.getCursorPosition(e);
        if (cell.row == canvas.data.original.row && cell.col == canvas.data.original.col)
            return;
        var ctx = canvas.getContext('2d');
        if (canvas.isSelected(cell)) {
            canvas.removeCell(cell);
            canvas.clearSelected(ctx, cell);
        }
        else {
            canvas.addCell(cell);
            canvas.drawSelected(ctx, cell);
        }
        var answer = '';
        for (var i = 0; i < data.selection.length; i++) {
            if (i > 0) answer += ',';
            answer += '{ row: ' + data.selection[i].row + ', col: ' + data.selection[i].col + ' }'
        }
        CQ.setAnswer($id$,answer);
    }
    
    for (var row = 0; row < 8; row++) {
        for (var col = 0; col < 8; col++) {
            var color = (row + col) % 2;
            if (color == 0)
                ctx.fillStyle = 'rgb(255,255,255)';
            else
                ctx.fillStyle = 'rgb(140,140,140)';
            ctx.fillRect(col * 50, row * 50, 50, 50);
        }
    }

    for (var i = 0; i < data.selection.length; i++)
        canvas.drawSelected(ctx, data.selection[i]);

    ctx.font = '48px sans-serif';
    ctx.fillStyle = 'rgb(0,0,0)';
    var text = String.fromCharCode(data.figure);
    var m = ctx.measureText(text);
    ctx.fillText(text, data.original.col * 50 + (50-m.width)/2 , (data.original.row+1) * 50 - 5);  
        
    if (data.active) 
        canvas.addEventListener('click', canvas.chessCanvasOnClick, false);
}
</script>
";

The GetBody method above also uses the GetFigure method, which simply returns the unicode for the characters representing black chess piece for the name.

private int GetFigure(string name)
{
    switch (name)
    {
        case "queen": return 9819;
        case "rook": return 9820;
        case "bishop": return 9821;
        case "knight": return 9822;
    }
    // Bad name, return a pawn
    return 9823;
}
See Also
Advanced Assessment Editor <custom> Javascript API Web Service API