Remote attribute does't work properly

2033

Member
Joined
May 11, 2025
Messages
7
Programming Experience
1-3
I have a viewModel for the Index view. The problem is that when remote attribute is trying to do it's job the IsUnique method in my controller receives name = null, though i expext it to be a value that I put inside input field.

It works with another view, model of which is just SetViewModel. And here i have this vm inside another vm. On the index page I have sets in total as well as a button to create a new one. So I decided to create a vm, that contains everything for the view.

This is a viewModel for the index view. Here I want you to notice the SetViewModel vm, that contains validation attributes (Remote as well):
public class IndexViewModel
{
    public IEnumerable<SetViewModel> Sets { get; set; }

    public SetViewModel NewSet { get; set; }

    public IndexViewModel()
    {
        Sets = [];
        NewSet = new();
    }
}

SetViewModel itself with the property Name:
public class SetViewModel
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Name is required.")]
    [Display(Name = "Name of the set")]
    [MaxLength(2, ErrorMessage = "The name of the set must be with a maximum length of {0} characters")]
    [Remote(action: "CheckSet", controller: "Home", areaName: "Sets", HttpMethod = "POST", AdditionalFields = "Id", ErrorMessage = "Set with this name already exists")]
    public string Name { get; set; } = null!;
}

Method IsUnique in a cotroller. The parameter name is null from index view, but it's working in another view with just SetViewModel as a model.:
[HttpPost]
public async Task<IActionResult> CheckSet(string name, int id)
{
    return Json(!await IsSetUnique(name, id));
}

What am I doing wrong in this scenario? Why is it null? When I fill the input field and submit the form it shouldn't be null. In the POST method it comes with a name, but not here.
 
What is the model that you set in the view?
 
What is the model that you set in the view?

As I said in the beginning the viewModel of a view Index is IndexViewModel. There I have a form. When I submit it, it goes in the next method.
C#:
[HttpPost]
public async Task<IActionResult> AddSet(IndexViewModel vm)
{
    await dataManager.SetRepository.AddAsync(mapper.Map<Set>(vm.NewSet));

    TempData["success"] = "The new set has been successfully added";
    return RedirectToAction("Index");
}

Well, not entirerly. Firstly it goes to check the remote action, that I have in SetViewModel, but the name in the CheckSet method is null for some strange reason. Because after the CheckSet method it goes in to the AddSet method and there I can see that property Name is populated from an input.
 
@Skydiver To be honest it seems like I must use ajax if I want to achieve anything good (though i was trying not to). Just need to get rid of the remote attribute (doesn't work with ajax for some reason)
 
My question was meant to prompt you to show us your CSHTML. Basically what did you have for your @model line in the CSHTML?

Anyway, Razor has no way of knowing that the name parameter of your CheckSet() method maps to the the Name property of the NewSet property of your IndexViewModel model. The typical way to get Razor to know this is to use partial views. Within the partial view, you would set the model to be the SetViewModel.
 
My question was meant to prompt you to show us your CSHTML. Basically what did you have for your @model line in the CSHTML?

Bro, as I said IndexViewModel:
@model IndexViewModel
@{
    ViewData["Title"] = "Flashcards";
}

<div class="row pt-4 pb-3">

    <h2 class="text-light mb-0 col-6 align-content-center">
        Your library
    </h2>

    <div class="col-6 text-end">
        <a class="btn btn-primary border-1 border-dark-subtle text-bg-dark" title="Create new set" data-bs-toggle="modal" data-bs-target="#newset">
            <i class="bi bi-plus-lg"></i> Add new set
        </a>
    </div>

    <!-- Modal for add set action -->
    <form asp-area="Sets" asp-controller="Home" asp-action="AddSet" method="post">

        <input hidden asp-for="NewSet.UserId" />

        <div class="modal fade" id="newset" tabindex="-1" aria-labelledby="lableIMG" aria-hidden="true" data-bs-theme="dark">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title text-light" id="lableIMG">New set</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body">
                        <div class="input-group row m-0 p-0">
                            <label asp-for="@Model.NewSet.Name" class="input-group-text bg-dark text-light"></label>
                            <input asp-for="@Model.NewSet.Name" class="form-control bg-dark text-light">
                            <span asp-validation-for="@Model.NewSet.Name" class="text-danger"></span>
                        </div>
                    </div>
                    <div class="modal-footer justify-content-center">
                        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>

                        <button type="submit" class="btn btn-primary">
                            Create new set
                        </button>

                    </div>
                </div>
            </div>
        </div>
    </form>
 
Interesting that this blog post says that the property being validated by Remote needs to be a direct descendant, but I can't find corresponding MS documentation to back it up. I wonder if it was hard earned experience that led the author to that statement.

 
It seems to lie in how the ASP.NET Core MVC's model binding and unobtrusive validation handle names for nested properties compared to top-level properties.

How [Remote] and Unobtrusive Validation Work:
When you use asp-for on a property decorated with [Remote], the tag helper generates an HTML <input> element. This input element gets a name attribute. For nested properties like NewSet.Name, the generated name attribute will be "NewSet.Name".

The [Remote] attribute adds special data-val-remote-* attributes to this input tag. These attributes contain the URL to call (/Sets/Home/CheckSet), the HTTP method, and importantly, information about additional fields to send (AdditionalFields).

Client-side unobtrusive JavaScript validation reads these data-val-* attributes. When the value in the input field changes (and depending on validation settings, often on blur or form submission), the script triggers an AJAX request to the specified URL.

When making the AJAX call, the script reads the name attribute of the input field being validated (which is "NewSet.Name") and sends its value using this name as the key in the request body or query string (e.g., NewSet.Name=value).

If AdditionalFields is specified (like "Id"), the script looks for other input fields with the exact names listed (or names that match the naming convention for nested properties) and sends their values using their names as keys (e.g., if there's a field named "Id", it sends Id=value).
Model Binding Mismatch:
Your CheckSet action method is defined as public async Task<IActionResult> CheckSet(string name, int id).
When the AJAX request arrives from the client-side validation, the incoming data includes NewSet.Name=someValue.
The default model binder tries to match the incoming keys (NewSet.Name) to the action method parameter names (name).
Since "NewSet.Name" does not match "name", the model binder fails to bind the value to the name parameter, leaving it null.
For the id parameter, the same issue applies. You have AdditionalFields = "Id" in your Remote attribute. However, the corresponding input field in your form generated for NewSet.Id (or NewSet.UserId as shown in your sample) would have a name like "NewSet.Id" or "NewSet.UserId". The client-side script looks for a field with the exact name "Id" and likely doesn't find one, so it doesn't send the ID data at all, or sends it with the wrong key.
Why it Worked Before (with just SetViewModel):
If your view model was simply SetViewModel (@model SetViewModel), the asp-for="Name" tag helper would generate an input with name="Name".
The AJAX call for Remote validation would send Name=someValue.
The CheckSet(string name, int id) action method would receive Name=someValue. The model binder can match the incoming key "Name" to the parameter "name" (this is a standard convention or simple direct match), so the name parameter was correctly populated.
Similarly, if you had a field for Id with name="Id" and AdditionalFields = "Id", the ID would also bind correctly.

Try this:

You need to adjust your CheckSet action method parameter names to match the names the model binder expects when dealing with data from nested properties.

When the model binder sees a request parameter named Parent.Child, it attempts to bind it to an action parameter named Parent_Child (using an underscore).

Modify your CheckSet Action Method: Change the parameter names to reflect the structure NewSet.Name and NewSet.Id.
Modify:
[HttpPost]
public async Task<IActionResult> CheckSet(string NewSet_Name, int NewSet_Id) // Parameter names match generated input names
{
    // Use the correctly bound parameters here
    return Json(!await IsSetUnique(NewSet_Name, NewSet_Id));
}

Note: I've used NewSet_Id assuming you intend to send the NewSet.Id value. If you meant to send the value of NewSet.UserId, the parameter should be int NewSet_UserId or string NewSet_UserId depending on the property type, and you'd also need to ensure AdditionalFields is set to "NewSet.UserId").
Modify the AdditionalFields in the [Remote] attribute: You also need to ensure the AdditionalFields correctly refers to the generated name of the ID field. Assuming the hidden input for the ID is generated using asp-for="NewSet.Id", its name will be "NewSet.Id".

Modify the AdditionalFields:
public class SetViewModel{
    public int Id { get; set; } // Make sure this property exists and is used in the form

    [Required(ErrorMessage = "Name is required.")]
    [Display(Name = "Name of the set")]
    [MaxLength(2, ErrorMessage = "The name of the set must be with a maximum length of {0} characters")]
    [Remote(action: "CheckSet", controller: "Home", areaName: "Sets", HttpMethod = "POST", AdditionalFields = "NewSet.Id", ErrorMessage = "Set with this name already exists")] // Changed AdditionalFields
    public string Name { get; set; } = null!;
    // Assuming UserId is not the ID you need for uniqueness check?
    public string UserId { get; set; } // Keep UserId if needed for AddSet, but use Id for the remote check
}

And ensure you have a hidden input field for NewSet.Id in your form:
NewSet.Id:
<input type="hidden" asp-for="NewSet.Id" />
In your provided CSHTML, you have asp-for="NewSet.UserId", not NewSet.Id. If you intend to send NewSet.UserId as the "ID" for the uniqueness check, change AdditionalFields to "NewSet.UserId" and the CheckSet parameter to string NewSet_UserId.


In your provided CSHTML, you have asp-for="NewSet.UserId", not NewSet.Id. If you intend to send NewSet.UserId as the "ID" for the uniqueness check, change AdditionalFields to "NewSet.UserId" and the CheckSet parameter to string NewSet_UserId.
I would try to look into this, I'm not always right but I believe it's the issue.
 
Back
Top Bottom