10/17/2020 Admin

Blazor Simple Survey: Creating Dynamic Surveys


You can create dynamic pages that will capture input from users using controls such as Text Box, Text Area, Date, Date Time, Dropdown, and Multi-Select Dropdown. This is demonstrated in the Server Side Blazor application Simple Survey available at https://github.com/ADefWebserver/BlazorSimpleSurvey.

Walk-Thru

image

User can log into the application using any provider supported by Microsoft Azure B2C.

image

Users can take surveys created by the application administrator.

image

Survey takers and visitors can view the tabulated results in pie charts.

image

Application administrators create and edit the surveys using the Survey Administration screen.

 

Set-Up

image

To set-up Blazor Simple Survey, the first step is to create a database and run the .sql scripts in !SQL directory.

image

Next, edit the appsettings.json to set the database connection (the DefaultConnection in the ConnectionStrings section).

image
Blazor Simple Survey requires Azure B2C (even when running on your local development machine).

To setup Azure B2C, follow the directions at: Azure AD B2C Quickstart with Visual Studio & Blazor, and then copy the settings to the appsettings.json file (in the AzureAdB2C section).

image

To create surveys, a user account needs to be in the Blazor Simple Survey Administrators group.

This is done using the Azure B2C user administration page (See: Blazor Azure B2C User And Group Management for full directions).

Create Surveys

image

When a user in the configured Blazor Simple Survey Administrators group logs in, they will see the Survey Administration link.

Clicking on this link takes them to the page that allows them to click the button to create a new Survey.

image

A popup shows that allows them to give the Survey a name.

image

Clicking the plus button allows items to be added to the Survey.

image

A popup displays to allow items to be added or edited.

image

If the selected item type is Dropdown or Multi-Select Dropdown, the Edit Options button appears.

image

This displays a popup that allows the dropdown options to be added or edited.

image

Reorder buttons allow the position of the survey items to be adjusted.

image

The Survey page allows users to select, complete, and Submit a survey.

image

The Responses tab allows users to see the tabulated results, of all responses, for all the survey controls except the Text Area control.

Data Layer

image

The application consists of six tables.

image

EF Core Power Tools was used to create the SimpleSurveyContext Data Context.

The SimpleSurveyService uses the Data Context to connect to the database and consists of methods that allow the remaining code to perform data functions.

Creating And Editing Survey

The following button opens the EditSurvey control using the Radzen Dialog:

image

<RadzenButton ButtonStyle="ButtonStyle.Success"
                Size="Radzen.ButtonSize.Medium"
                MouseEnter="@(args => 
                            ShowTooltip(args, new TooltipOptions()
                            { Text= "New Survey" }))"
                Icon="addchart" Click="@(args =>
                    dialogService.Open<EditSurvey>($"New Survey",
                    new Dictionary<string, object>() { 
                        { 
                            "SelectedSurvey", new Survey() { Id = -1 } 
                        } 
                    },
                    new DialogOptions() { Width = "500px", Height = "280px" }))" />

 

image

The following markup displays the form that allows you to create or edit a survey.

The delete button will only display if the survey is not a new survey:

 

<RadzenCard Style="margin-bottom: 20px;">
    <div class="row">
        <div class="col-md-12">
            <div>Survey Name:</div>
            <RadzenTextBox @bind-Value="SelectedSurvey.SurveyName" 
                           Style="width: 400px" />
            <br />
        </div>
    </div>
</RadzenCard>
<RadzenButton Click="UpdateSurvey"
              Text="Save" ButtonStyle="ButtonStyle.Success"
              Style="margin-bottom: 10px; width: 150px" />
@if (SelectedSurvey.Id > 0)
{
    <RadzenButton Click="DeleteSurveyDialog"
                  ButtonStyle="ButtonStyle.Danger"
                  Text="Delete"
                  Style="margin-bottom: 10px; width: 150px" />
}

 

When the survey is updated, the following code is used to call the Data Context:

 

    async Task UpdateSurvey()
    {
        SelectedSurvey.UserId = UserId;
        try
        {
            if (SelectedSurvey.Id == -1)
            {
                SelectedSurvey = 
                    await @Service.CreateSurveyAsync(SelectedSurvey);
            }
            else
            {
                SelectedSurvey = 
                    await @Service.UpdateSurveyAsync(SelectedSurvey);
            }
            dialogService.Close(SelectedSurvey);
        }
        catch (Exception ex)
        {
            strError = ex.GetBaseException().Message;
        }
    }

 

Survey Items

image

The following markup displays the form that allows survey items to be defined:

 

   <div class="row">
        <div class="col-md-12">
            <div><b>Name</b></div>
            <RadzenTextBox @bind-Value="SelectedSurveyItem.ItemLabel"
                           Style="width: 400px" />
            <br />
        </div>
    </div>
    <br />
    <div class="row">
        <div class="col-md-6">
            <div><b>Type</b></div>
            <RadzenDropDown AllowClear="true" TValue="string"
                            Data="@FormTypes"
                            @bind-Value="SelectedSurveyItem.ItemType" />
        </div>
        <div class="col-md-6">
            <div><b>Required</b></div>
            <RadzenCheckBox @bind-Value="boolRequired"
                            Style="margin-bottom: 20px" TValue="bool" />
        </div>
    </div>

 

image

If a survey administrator selects a Dropdown or a Multi-Select Dropdown a button to edit options is displayed:

 

    <div class="row">
        <div class="col-md-12">
            @if ((SelectedSurveyItem.ItemType == "Dropdown") 
                || (SelectedSurveyItem.ItemType == "Multi-Select Dropdown"))
            {
                <RadzenButton Click="OpenPopup"
                              Text="Edit Options" 
                              Size="Radzen.ButtonSize.Small"
                              ButtonStyle="ButtonStyle.Info"/>
            }
        </div>
    </div>

 

image

This opens a Popup that displays a modal form to allow the options to be edited:

 

@if (ShowPopup)
{
    <div class="modal" tabindex="-1" 
         style="display:block;background-color:gainsboro" role="dialog">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h3 class="modal-title">Edit Options</h3>
                </div>
                <div class="modal-body">
                    <ul style="list-style-type:none;">
                        @foreach (var option in 
                            SelectedSurveyItem.SurveyItemOption.OrderBy(x => x.Id))
                        {
                            <li>
                                <div Style="margin-bottom: 10px; width: 150px" >
                                <RadzenButton Click="(() => RemoveOption(option))"
                                              Icon="delete"
                                              Size="Radzen.ButtonSize.Small"
                                              ButtonStyle="ButtonStyle.Danger" />
                                <b>@option.OptionLabel</b>
                                </div>
                            </li>
                        }
                    </ul>
                    <RadzenTextBox @bind-Value="newOption"
                                   Style="width: 200px" />
                    <RadzenButton Click="AddOption"
                                  Text="Add"
                                  Size="Radzen.ButtonSize.Small"
                                  ButtonStyle="ButtonStyle.Success" />
                    <RadzenButton Click="ClosePopup"
                                  Text="Close"
                                  Size="Radzen.ButtonSize.Small"
                                  ButtonStyle="ButtonStyle.Secondary" />
                </div>
            </div>
        </div>
    </div>
}

 

image

If a survey item is selected for deletion, the Radzen dialog allows the conformation to be handled in a single method:

 

    async Task DeleteSurveyItemDialog() => 
        await dialogService.OpenAsync("Delete Survey Item", ds => 
    @<RadzenCard Style="padding: 20px;">
        <p Style="margin-bottom: 10px;">Confirm?</p>
        <div class="row">
            <div class="col-md-12">
                <RadzenButton Text="Yes" Click="DeleteSurveyItem"
                              Style="margin-bottom: 10px; width: 150px" />
                <RadzenButton Text="No" Click="()=> ds.Close(false)"
                              ButtonStyle="ButtonStyle.Secondary"
                              Style="margin-bottom: 10px; width: 150px" />
            </div>
        </div>
    </RadzenCard>);

 

Display Survey

image

When displaying a survey, a class is used (DTOSurvey) that contains fields to display the survey as well as fields that can be bound to controls to capture the responses to the survey items.

image

The following markup displays a dropdown to allow the visitor to select a survey:

 

<RadzenDropDown TValue="DTOSurvey"
                Data="@colSurveys"
                TextProperty="SurveyName"
                @bind-Value="SelectedSurvey"
                Change="@(args => SelectedSurveyChange(args))" />

 

This calls the following method that loads the selected survey, and then also calls the LoadSurveyResultsData method, to load the tabulated results for the selected survey.

Before it does, it creates default Radzen.LoadDataArgs that provide data filtering:

 

    async Task SelectedSurveyChange(object value)
    {
        ShowSurveyComplete = false;
        colSurveys = new List<DTOSurvey>();
        await RefreshSurveys(SelectedSurvey.Id);
        // Refresh results
        LoadDataArgs args = new LoadDataArgs();
        args.Filter = null;
        args.OrderBy = null;
        args.Skip = 0;
        args.Top = 1;
        await LoadSurveyResultsData(args);
    }

 

The LoadSurveyResultsData method uses the Radzen.LoadDataArgs to filter and sort the data.

The reason this is constructed this way, is that in the remaining code, the RadzenDataList, that is used to allow the visitor to page through the pie charts, is also bound to this method, and it will construct and pass Radzen.LoadDataArgs to this method.

 

        // Get Survey Items
        // Don't include "Text Area"
        var query = dbContext.SurveyItem
            .Where(x => x.Survey == SelectedSurvey.Id)
            .Where(x => x.ItemType != "Text Area")
            .Include(x => x.SurveyItemOption)
            .OrderBy(x => x.Position).AsQueryable();
        if (!string.IsNullOrEmpty(args.Filter))
        {
            query = query.Where(args.Filter);
        }
        if (!string.IsNullOrEmpty(args.OrderBy))
        {
            query = query.OrderBy(args.OrderBy);
        }
        var Results = query.Skip(args.Skip.Value)
            .Take(args.Top.Value).ToList();

 

The survey items are iterated and added to the collection in the DTOSurvey object.

The tabulated results, used to display in the pie charts, is constructed using the following code:

 

            ColAnswerResponse = dbContext.SurveyAnswer
                .Where(x => x.SurveyItemId == SurveyItem.Id)
                .GroupBy(n => n.AnswerValue)
                .Select(n => new AnswerResponse
                {
                    OptionLabel = n.Key,
                    Responses = n.Count()
                }
                ).OrderBy(n => n.OptionLabel).ToList();
        }
        if(ColAnswerResponse.Count > 10)
        {
            // Only take top 10 
            NewDTOSurveyItem.AnswerResponses = ColAnswerResponse
                .OrderByDescending(x => x.Responses)
                .Take(10).ToList();
            // Put remaining items in "Other"
            var ColOtherItems = 
                ColAnswerResponse.OrderByDescending(x => x.Responses)
                .Skip(10)
                .ToList();
            var SumOfOther = 
                ColOtherItems.Sum(x => x.Responses);
            NewDTOSurveyItem.AnswerResponses.Add(new AnswerResponse() 
            { 
                OptionLabel = "Other", Responses = SumOfOther 
            });
        }
        else
        {
            NewDTOSurveyItem.AnswerResponses = ColAnswerResponse;
        }
        SurveyResultsCollection.Add(NewDTOSurveyItem);

 

image

This calls the following method when the survey is submitted:

 

    async Task SaveSurvey()
    {
        try
        {
            var result =
                    await @Service.CreateSurveyAnswersAsync(SelectedSurvey);
            CompleteSurvey();
        }
        catch (Exception ex)
        {
            strError = ex.GetBaseException().Message;
        }
    }

 

That calls the following method to save the submission to the database:

 

public Task<bool> CreateSurveyAnswersAsync(DTOSurvey paramDTOSurvey)
{
    try
    {
        List<SurveyAnswer> SurveyAnswers = new List<SurveyAnswer>(); 
        foreach (var SurveyItem in paramDTOSurvey.SurveyItem)
        {
            // Delete possible existing answer
            var ExistingAnswers = _context.SurveyAnswer
                .Where(x => x.SurveyItemId == SurveyItem.Id)
                .Where(x => x.UserId == paramDTOSurvey.UserId)
                .ToList();
            if(ExistingAnswers != null)
            {
                _context.SurveyAnswer.RemoveRange(ExistingAnswers);
                _context.SaveChanges();
            }
            // Save Answer
            if (SurveyItem.ItemType != "Multi-Select Dropdown")
            {
                SurveyAnswer NewSurveyAnswer = new SurveyAnswer();
                NewSurveyAnswer.AnswerValue = 
                    SurveyItem.AnswerValueString;
                NewSurveyAnswer.AnswerValueDateTime = 
                    SurveyItem.AnswerValueDateTime;
                NewSurveyAnswer.SurveyItemId = 
                    SurveyItem.Id;
                NewSurveyAnswer.UserId = 
                    paramDTOSurvey.UserId;
                _context.SurveyAnswer.Add(NewSurveyAnswer);
                _context.SaveChanges();
            }
            if (SurveyItem.AnswerValueList != null)
            {
                foreach (var item in SurveyItem.AnswerValueList)
                {
                    SurveyAnswer NewSurveyAnswerValueList = 
                        new SurveyAnswer();
                    NewSurveyAnswerValueList.AnswerValue = 
                        item;
                    NewSurveyAnswerValueList.SurveyItemId = 
                        SurveyItem.Id;
                    NewSurveyAnswerValueList.UserId = 
                        paramDTOSurvey.UserId;
                    _context.SurveyAnswer
                        .Add(NewSurveyAnswerValueList);
                    _context.SaveChanges();
                }
            }
        }
        return Task.FromResult(true);
    }
    catch
    {
        DetachAllEntities();
        throw;
    }
}

 

image

The markup to display the survey is as follows:

 

<RadzenTemplateForm TItem="DTOSurvey" Data="@SelectedSurvey" Submit="@SaveSurvey">
    <div>
        @foreach (var SurveyItem in SelectedSurvey.SurveyItem.OrderBy(x => x.Position))
        {
            <div class="row">
                <div class="col-md-9" style="text-align: left;margin-bottom: 20px">
                    <b>@SurveyItem.ItemLabel</b>
                    <br />
                    @if (SurveyItem.ItemType == "Text Box")
                    {
                        <RadzenTextBox MaxLength="4000" Name="@SurveyItem.Id.ToString()"
                                        @bind-Value="@SurveyItem.AnswerValueString" />
                    }
                    @if (SurveyItem.ItemType == "Text Area")
                    {
                        <RadzenTextArea MaxLength="4000" Name="@SurveyItem.Id.ToString()"
                                        @bind-Value="@SurveyItem.AnswerValueString" />
                    }
                    @if (SurveyItem.ItemType == "Date")
                    {
                        <RadzenDatePicker DateFormat="d" Name="@SurveyItem.Id.ToString()"
                                            @bind-Value="@SurveyItem.AnswerValueDateTime"
                                            TValue="DateTime?" ShowTime="false" />
                    }
                    @if (SurveyItem.ItemType == "Date Time")
                    {
                        <RadzenDatePicker TValue="DateTime?" Name="@SurveyItem.Id.ToString()"
                                            @bind-Value="@SurveyItem.AnswerValueDateTime"
                                            ShowTime="true" />
                    }
                    @if (SurveyItem.ItemType == "Dropdown")
                    {
                        <RadzenDropDown AllowClear="true" TValue="string" 
                                        Name="@SurveyItem.Id.ToString()"
                                        Data="@SurveyItem.SurveyItemOption.OrderBy(x => x.Id)"
                                        TextProperty="OptionLabel" ValueProperty="OptionLabel"
                                        @bind-Value="@SurveyItem.AnswerValueString"
                                        Style="width:300px;" />
                    }
                    @if (SurveyItem.ItemType == "Multi-Select Dropdown")
                    {
                        <RadzenDropDown Name="@SurveyItem.Id.ToString()"
                                        TValue="IEnumerable<string>"
                                        Multiple="true"
                                        AllowClear="true"
                                        AllowFiltering="true"
                                        @bind-Value="SurveyItem.AnswerValueList"
                                        Placeholder="Select..."
                                        Data="@SurveyItem.SurveyItemOption"
                                        TextProperty="OptionLabel"
                                        ValueProperty="OptionLabel"
                                        Style="width:300px;" />
                    }
                    @if (SurveyItem.Required == 1)
                    {
                        <br /><RadzenRequiredValidator Component="@SurveyItem.Id.ToString()"
                                                        Text="Required" Popup="false"
                                                        Style="position: absolute" />
                    }
                </div>
            </div>
        }
    </div>
    @if (isAuthenticated)
    {
        <div class="row">
            <RadzenButton ButtonType="ButtonType.Submit" Text="Submit"></RadzenButton>
        </div>
    }
    else
    {
        <h4>You must <a href="AzureADB2C/Account/SignIn">Log in</a> to take a Survey</h4>
    }
</RadzenTemplateForm>

 

image

The markup used to display the survey results is as follows:

 

<RadzenDataList Count="@SurveyResultsCount"
                Data="@SurveyResults"
                LoadData="@LoadSurveyResultsData"
                PageSize="1" WrapItems="true" AllowPaging="true"
                TItem="DTOSurveyItem">
    <Template Context="item">
        <RadzenCard Style="height:300px">
            <div class="row">
                <div class="col-md-12">
                    <RadzenChart>
                        <RadzenDonutSeries Data="@item.AnswerResponses"
                                            CategoryProperty="OptionLabel"
                                            ValueProperty="Responses">
                            <TitleTemplate>
                                <div class="rz-donut-content">
                                    <div>@item.ItemLabel</div>
                                </div>
                            </TitleTemplate>
                        </RadzenDonutSeries>
                    </RadzenChart>
                </div>
            </div>
        </RadzenCard>
    </Template>
</RadzenDataList>

 

 

Links

Blazor Simple Survey (GitHub)

Blazor Multi-Tenant Azure B2C

Blazor Azure B2C User And Group Management

Creating A Blazor Server Azure B2C App

Free Blazor Components | 50+ controls by Radzen

An unhandled error has occurred. Reload 🗙