4
\$\begingroup\$

Business Logic

I have a company referral form, and in that form I have dropdown to select from a company list. But when a company is not found in the dropdown, the user can select option as "others", upon which a textbox will appear. Now the user can add the company in the textbox and will fill rest of the "referral form". Once the form is submitted, it will add that "new company" added by the user in my company list table so that from next time that company option is available in the dropdown. Also, I have a cover letter tied to all the companies. As soon as the user selects a company from the dropdown, I make an Ajax call to fetch the cover letters for that particular company.

Model

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

    public bool IsResumeExists { get; set; }

    public bool IsCoverLetterExists { get; set; }
    // ********* Issue No 1 ************
    [RequiredIf("CompanyId", "4", ErrorMessage = "Enter Company Name")]
    [Display(Name = "Other Company")]
    public string TempCompany { get; set; }

    [Display(Name = "Resume")]
    [Required]
    public int ResumeId { get; set; }
    public IEnumerable<SelectListItem> Resumes { get; set; }

    // preventing from underposting attack
    [Display(Name = "Company Name")]
    [Required]
    public int? CompanyId { get; set; }
    public IEnumerable<SelectListItem> Companies { get; set; }

    // needs to have "?" because when no dropdown is selected we want to pass NULL values
    // question marks makes sure no default value is set by the framework.
    [Display(Name = "Cover Letter")]
    public int? CoverLetterId { get; set; }
    public IEnumerable<SelectListItem> CoverLetters { get; set; }
}

View

@model Bridge.ViewModels.ReferralViewModel

@{
    ViewBag.Title = "Create";
}

@using (Html.BeginForm("AddReferral",null,FormMethod.Post, new { id = "referralform", data_url = Url.Action("CheckForExistingReferral", "Referral") }))
{
    @Html.AntiForgeryToken()

    if (!Model.IsResumeExists)
    {
        <div class="alert alert-info">
            <strong>No Resume Found!</strong> Click on <u><strong>@Html.ActionLink("Resume Center", "ResumeCenter", "Resume")</strong></u> and upload your Resume first.
        </div>
    }
    else
    {
        <div class="form-horizontal">
            <h4>Job Request</h4>
            <hr />
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })

            <div class="form-group">
                @Html.LabelFor(model => model.CompanyId, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.DropDownListFor(m => m.CompanyId, Model.Companies, "Please Select", new { @class = "form-control", data_url = Url.Action("ListOfCoverLetterByCompanyId", "Referral") })
                    @Html.ValidationMessageFor(model => model.CompanyId, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group js-div" style="display:none;">
                @Html.LabelFor(model => model.TempCompany, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.TempCompany, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.TempCompany, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.ResumeId, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.DropDownListFor(m => m.ResumeId, Model.Resumes, "Please Select", new { @class = "form-control" })
                    @Html.ValidationMessageFor(model => model.ResumeId, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group">
                @Html.LabelFor(model => model.CoverLetterId, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.DropDownListFor(m => m.CoverLetterId, Model.CoverLetters, "Please select", new { @class = "form-control" })
                    @Html.ValidationMessageFor(model => model.CoverLetterId, "", new { @class = "text-danger" })
                </div>
            </div>

            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Create" class="btn btn-default" />
                </div>
            </div>
        </div>
    }
}

Script

$(function () {
    var coverLetterSelect = $('#CoverLetterId');
    $('#CompanyId').change(function () {
        var companyId = $(this).val();
        var url = $(this).data('url');
        coverLetterSelect.empty();
        if (!companyId) {
            return;
        }
         // ********* Issue No 2 ************
        if (companyId == 4)
        {
            $('.js-div').show();
        }
        else {
            $('.js-div').hide();
        }

        // ********* Issue No 3 ************
        $.ajax({
            url: url,
            type: 'POST',
            data: { companyId: companyId },
            success: function (response) {
                coverLetterSelect.append($('<option></option>').val('').text('None'));
                $.each(response, function (i, data) {
                    coverLetterSelect.append($('<option></option>').val(data.Value).text(data.Text));
                });
            },
            error: function () { }
        });
    });
})

Controller Action

   [HttpPost]
    public JsonResult ListOfCoverLetterByCompanyId(int companyId)
    {
        var coverletters = _context.CoverLetters
            .Where(c => c.CompanyId == companyId).Select(c => new
            {
                Value = c.CoverLetterId.ToString(),
                Text = c.CoverLetterName
            });

        return Json(coverletters);
    }

Issue: All these issue have also been marked in code. Scroll to the code to find them.

  1. My foolproof plug's REQUIREDIF is based on the fact that Option 4. Whenever selected, that means the user is going to add a new company, which is working well in my local environment but will not work in a live app.

  2. In JavaScript I am checking again for "Option 4" and showing the user a temporary textbox for adding a new company dropdown option.

  3. Unnecessary Ajax call even when the user has selected "others". We are sure when the user is selecting others as company then there will be no cover letter for it, so no need to make an Ajax call.

  4. Basically, I have hardcoded in the database "Option 4" as a company named "others", but I think that is a bad approach. When I publish the app to live, that Option 4 will not be there.

Please help me with adding "others" in the dropdown in an elegant way. Also, note that I explicitly want to follow showing a textbox to add a company approach.

enter image description here

enter image description here

\$\endgroup\$
1
\$\begingroup\$

To address points 1, 2 and 4, You database should not include a value for "Others". Instead you can handle this one of 2 ways

Option 1

Remove the [Required] attribute from CompanyId and change the attribute for the TempCompany to

[RequiredIfEmpty("CompanyId", ErrorMessage = "Enter a Company Name or select from the above")]
[Display(Name = "Other Company")]
public string TempCompany { get; set; }

and change the view to

@Html.DropDownListFor(m => m.CompanyId, Model.Companies, "Please Select or enter below", new { @class = "form-control"

This will force the user to either select one of the existing companies or enter a new company name in the textbox.

Option 2

If you want to display both "Please Select" and "Other" options, then in your GET method, insert a SelectListItem into your Companies property, for example

var companies = db.Companies.Select(x => new SelectListItem
{
    Value = x.CompanyId.ToString(),
    Text = x.CompanyName
}).ToList();
companies.Insert(0, new SelectListItem{ Value = "0", Text = "Other" });
model.Companies = companies;

Now you can change the attribute to

[RequiredIf("CompanyId", "0", ErrorMessage = "Enter Company Name")]

Note this assumes you will not have a value for CompanyId of 0 in the database

To address point 3, if you use option 1 above, then no ajax call will be made since you already have

if (!companyId) {
    return;
}

which will exit the function. If you use option 2 above, then change the code to only execute the ajax call if the selected option is not 0

if (companyId == 0)
{
    $('.js-div').show();
}
else {
    $('.js-div').hide();
    $.ajax({
        ....
    });
}

While this will solve your immediate issues, your approach of using a dropdownlist to display all existing Companies is not a good one. Over time you Company table will build up and you may have thousands of records. This will have a detrimental affect on performance (to the point where it may crash the browser) and makes for a poor user experience (having to scroll through that many options).

A better approach would be to use a jquery autocomplete control. The view would include a hidden input for int? CompanyId and a textbox for string CompanyName. The plugin would be bound to the textbox, and configured to make an ajax call to server method once 1 or 2 characters have been entered, and the method would return existing Companies (both the CompanyId and CompanyName properties) starting with the search term.

If the user selects one of the returned values, then you set the value of the hidden CompanyId value so its sent in the request when the form is submitted. If the user continues to type a name for a new Company, then the value of CompanyId is not set, so in the POST method you can check if its null, and if so, add a new Company to the table.

\$\endgroup\$
  • \$\begingroup\$ I think bracket in the end is missing here. companies.Insert(0, new SelectListItem{ Value = "0", Text = "Other" }; \$\endgroup\$ – Unbreakable Sep 9 '17 at 3:56
  • \$\begingroup\$ Also, Insert method is not getting recognized. :/ Do I need to reference any namespace \$\endgroup\$ – Unbreakable Sep 9 '17 at 3:57
  • \$\begingroup\$ Nevermind. I missed to add ".ToList" \$\endgroup\$ – Unbreakable Sep 9 '17 at 4:01
2
\$\begingroup\$

Basically, I have hardcoded in database "Option 4" as a Company Named "Others". But I think that is bad approach. When I publish the app to live. That option 4 will not be there.

I've run into this conundrum before too. From a business standpoint "Others" is not a real company. It's a user interface term used to change the behavior of the application, and as such really only belongs in the presentation layer of the application (your MVC web project).

I solved this by inventing new options and appending it to the list of options with a special value that is not used in the database. Numeric primary keys tend to be integers greater than zero. Many ORMs like Entity Framework and NHibernate use an id of zero to mean it is an unpersisted record/object. "Others" could be a value of -1 which is not valid for a persisted record, or a special value representing an unsaved entity. In fact, I've created enums before to encapsulate these values:

public enum CompanySpecialOption
{
    Others = -1
}

Now you have a named constant to refer to the thing it is. When generating the collection of SelectListItem object you can insert the "Others" option manually:

IEnumerable<Company> companies = // get from database
List<SelectListItem> options = companies.Select(company => new SelectListItem() { Value = company.Id.ToString(), Text = company.Name })
    .ToList();

options.InsertAt(3, new SelectListItem()
{
    Value = ((int)CompanySpecialOption.Others).ToString(),
    Text = CompanySpecialOption.Others.ToString()
});

return options;

Now when processing this request, you have a named constant to compare things to on the C# side:

if (model.CompanyId == (int)CompanySpecialOption.Others)
{
    // Do something specific for the "Others" option
}
else
{
    // model.CompanyId is a real company from the database
}

This also gives you a named constant you can use in your views:

@Html.DropDownListFor => m.CompanyId, , Model.Companies, "Please Select", new
    {
        @class = "form-control",
        data_url = Url.Action("ListOfCoverLetterByCompanyId", "Referral"),
        data_others_id = (int)CompanySpecialOption.Others
    })

And JavaScript can look for this custom data-others-id attribute value:

var otherCompaniesId = this.getAttribute("data-others-id");

if (companyId === otherCompaniesId)
{
    $('.js-div').show();
}
else {
    $('.js-div').hide();

    // TODO: Send AJAX request for cover letters
}

Also, it feels like the else block is where you should be sending the AJAX request for cover letters.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.