Embed surveys in your ASP.NET Core application with SurveyJS

February 25, 2020
ASP.NET Core 3.1

Introduction

During a recent client engagement for a functional medicine practitioner, I had to allow users to complete a health profile questionnaire which involved around 40 health-related questions. The questions had many inter-dependencies; for example, we could ask follow-up questions based on the answer to a previous question.

Initially, I tried to do this with regular HTML and JavaScript and storing the data in a database table with each answer stored in a database column. It quickly became apparent that this was not going to work.

Firstly, trying the create the level of interactivity needed (hiding or showing follow-up questions based on previous answers) was going to require quite a bit of coding. Secondly, the practitioner I was developing the application for wanted to edit the questionnaire as they see fit, without requiring a developer to make the changes.

I started looking around on the internet for other ways to solve this and quickly came across a JavaScript library called SurveyJS. SurveyJS allows you to create surveys (i.e. questionnaires) easily and allows a high level of interactivity in these questionnaires, such as showing or hiding follow-up questions based on the answers of previous questions.

It also allows you to retrieve the answers of the survey as JSON, meaning we could store all the answers as a single JSON document inside the database.

In this blog post, I demonstrate how you can integrate SurveyJS into your own ASP.NET Core Razor Pages application.

Creating the base application

For demonstration purposes, let’s create a basic Razor Pages application with Individual User accounts.

dotnet new webapp -au Individual

We store the completed surveys for a user in a separate database table, so let’s define a new entity for that:

public class CompletedSurvey
{
    public int Id { get; set; }

    public string SurveyResult { get; set; }

    public IdentityUser User { get; set; }

    public string UserId { get; set; }
}

public class ApplicationDbContext : IdentityDbContext
{
    public DbSet<CompletedSurvey> CompletedSurveys { get; set; }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

We need to create a new database migration and update the database to apply the changes to our model.

dotnet ef migrations add AddCompletedSurvey
dotnet ef database update

Adding the SurveyJS library

Next, we can create a new Razor Page to allow a user to complete a survey. To integrate SureyJS into the page, I followed along with the SurveyJS documentation on adding SurveyJS to your web page.

It involves adding an HTML div element to host the survey, as well as including the survey script and bootstrapping the survey. You can follow along with the documentation I linked to above.

After following the documentation and integrating a basic survey into the application, this is what the code of the Razor page looks like:

@page
@model CompleteSurveyModel

<div class="row">
    <div class="col">
        <div id="surveyContainer"></div>
    </div>
</div>

@section Scripts
{
    <script src="https://surveyjs.azureedge.net/1.5.8/survey.jquery.min.js"></script>
    <script>
        var surveyJSON = { title: "Tell us, what technologies do you use?", questions: [
            { type: "radiogroup", choices: [ "Yes", "No" ], isRequired: true, name: "frameworkUsing",title: "Do you use any front-end framework like Bootstrap?" },
            { type: "checkbox", choices: ["Bootstrap","Foundation"], hasOther: true, isRequired: true, name: "framework", title: "What front-end framework do you use?", visibleIf: "{frameworkUsing} = 'Yes'" },
            { type: "radiogroup", choices: ["Yes","No"],isRequired: true, name: "mvvmUsing", title: "Do you use any MVVM framework?" },
            { type: "checkbox", choices: [ "AngularJS", "KnockoutJS", "React" ], hasOther: true, isRequired: true, name: "mvvm", title: "What MVVM framework do you use?", visibleIf: "{mvvmUsing} = 'Yes'" },
            { type: "comment", name: "about", title: "Please tell us about your main requirements for Survey library" }
         ]};

        Survey.StylesManager.applyTheme("bootstrap");
        var survey = new Survey.Model(surveyJSON);
        $("#surveyContainer").Survey({
            model:survey
        });
    </script>
}

When we run the application and navigate to the survey page, we can now see and complete the survey.

Complete the survey

Saving the survey result

At this moment, we allow a user to complement the survey, but we are not saving the survey result in the database yet. Let’s create a Razor Pages page handler to save the result.

public class CompleteSurveyModel : PageModel
{
    private readonly ApplicationDbContext _dbContext;

    public CompleteSurveyModel(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void OnGet()
    {

    }

    public async Task<IActionResult> OnPostSaveAsync(string data)
    {
        var userId = User.GetUserId();

        var surveyResult = await _dbContext.CompletedSurveys
            .Where(s => s.UserId == userId)
            .FirstOrDefaultAsync();
        if (surveyResult != null)
        {
            surveyResult.SurveyResult = data;
        }
        else
        {
            _dbContext.CompletedSurveys.Add(new CompletedSurvey
            {
                SurveyResult = data,
                UserId = userId
            });
        }
        await _dbContext.SaveChangesAsync();

        return new OkResult();
    }
}

In the Save handler above, we check to see whether there is a completed survey for the user. If so, we update that survey result with the data passed in the data parameter. If not, we add a new completed survey for the user.

We also have to call this handler from the frontend, so let’s update the JavaScript in the Razor Page to add an onComplete handler that calls a saveSurvey function:

$("#surveyContainer").Survey({
    model:survey,
    onComplete:saveSurvey
});

SurveyJS passes the completed survey as a JavaScript object in the survey parameter. We convert this value to a JSON string and post it to the Save page handler.

function saveSurvey(survey) {
$.ajax({
    type: "POST",
    url: '@LinkGenerator.GetUriByPage(HttpContext, null, "Save")',
    data: {
        data: JSON.stringify(survey.data)
    },
    headers: {
        "RequestVerificationToken": "@Xsrf.GetAndStoreTokens(HttpContext).RequestToken"
    }
})
.done(function() {
    window.location.href = '/';
});

Note that Razor Pages uses anti-forgery tokens to protect websites against Cross-site request forgery (CSRF) attacks, so we need to pass the request verification token along. You can read more about this in an earlier blog post about Sending an anti-forgery token with Razor Pages AJAX requests.

With this in place, we can rerun the application. Now, when we complete a survey, the result is saved to the database.

Survey stored in the database

Retrieving a survey result

At this moment, we do not retrieve existing answers when a user completes a survey. It would be useful if we can initialize the survey with the existing answers if a user has completed the survey previously.

To initialize a survey with current answers, we can set the data property when instantiating the Survey object in JavaScript (refer to SurveyJS docs).

The first thing we need to do is load the current survey result from the database and store it in a page model property so we can access it from the Razor page markup.

public class CompleteSurveyModel : PageModel
{
    public string SurveyResult { get; set; }

    public async Task OnGetAsync()
    {
        var userId = User.GetUserId();

        var surveyResult = await _dbContext.CompletedSurveys
            .Where(s => s.UserId == userId)
            .FirstOrDefaultAsync(); 

        SurveyResult = surveyResult?.SurveyResult ?? "{}";
    }
}

In the code snippet above, we have updated the page model’s OnGetAsync method to check if a completed survey exists for the user and store the previously saved JSON in the SurveyResult property. We must convert the JSON back to a JavaScript object, so if no survey result exists for the user, we set the SurveyResult property to an empty object ({}).

Let’s also update the JavaScript in the Razor page markup to store the survey result in a surveyResult variable and initialize the Survey object with this result by passing it in the data property:

let survey = new Survey.Model(surveyJSON);
let surveyResult = @Html.Raw(Model.SurveyResult);

$("#surveyContainer").Survey({
    model:survey,
    data: surveyResult,
    onComplete:saveSurvey
});

With this in place, we initialize the survey with the current answers, so when a user completes the survey for a second time, they see their previous answers and can update them.

Conclusion

In this blog post, I demonstrated how you could use the SurveyJS library to add interactive surveys to your ASP.NET Core application. I also demonstrated how to store the result of a survey in the database.

You can find the accompanying sample application for this blog post at https://github.com/jerriepelser-blog/using-surveyjs-with-aspnet-core.

PS: If you need assistance on any of your ASP.NET Core projects, I am available for hire for freelance work.