Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fluent Validation and MVC conflict #127

Closed
thiagomajesk opened this issue Sep 21, 2015 · 2 comments
Closed

Fluent Validation and MVC conflict #127

thiagomajesk opened this issue Sep 21, 2015 · 2 comments

Comments

@thiagomajesk
Copy link

I don't know, I'm probably missing something or there is a really weird behavior I'm not aware of...

I built three views with distinct viewModels and each one has its own validation class.
If I go to the two views I built first, leave all fields empty and submit the form, all validations are triggered correctlty. But the third one only triggers the validation for two fields
(ModelState.IsValid returns false and the ModelState only has errors for those two fields).

After a long time burning my brains off trying to understand the problem, I started changing the validations orders expecting a different result - no success. So I commented the rules and the validator attribute in the viewModel...
For my surprise, those two fields keep showing the validations even without no validation!!
I thought could be something related to cache - Changed browser, same result.

What am I missing here? The only two fields triggering validation: ClientId and DataPlan

<div class="row">
    <div class="col-md-12">
        @using (Html.BeginForm("Register", "Router", FormMethod.Post))
        {
            <div class="panel panel-primary">
                <div class="panel-heading"><h4>@ComponentNames.NewRouter</h4></div>
                <div class="panel-body">
                    <div class="row">
                        <div class="col-sm-6">
                            <div class="form-group-sm">
                                @Html.LabelFor(model => model.ClientId, new { @class = "control-label" })
                                @Html.ValidationMessageFor(model => model.ClientId, string.Empty, new { @class = "control-label", data_validation_for = $"#{nameof(Model.ClientId)}" })
                                @Html.DropDownListFor(model => model.ClientId, ViewBag.SelectListClients as IEnumerable<SelectListItem>, Misc.SelectOptionalLabel, new { @class = "form-control input-sm", id = $"{nameof(Model.ClientId)}" })
                            </div>
                        </div>
                        <div class="col-sm-6">
                            <div class="form-group-sm">
                                @Html.LabelFor(model => model.Name, new { @class = "control-label" })
                                @Html.ValidationMessageFor(model => model.Name, string.Empty, new { @class = "control-label", data_validation_for = $"#{nameof(Model.Name)}" })
                                @Html.TextBoxFor(model => model.Name, new { @class = "form-control input-sm", id = $"{nameof(Model.Name)}" })
                            </div>
                        </div>
                        <div class="col-sm-6">
                            <div class="form-group-sm">
                                @Html.LabelFor(model => model.MacAdrress, new { @class = "control-label" })
                                @Html.ValidationMessageFor(model => model.MacAdrress, string.Empty, new { @class = "control-label", data_validation_for = $"#{nameof(Model.MacAdrress)}" })
                                @Html.TextBoxFor(model => model.MacAdrress, new { @class = "form-control input-sm", id = $"{nameof(Model.MacAdrress)}", data_mask = "macaddress" })
                            </div>
                        </div>
                        <div class="col-sm-6">
                            <div class="form-group-sm">
                                @Html.LabelFor(model => model.RouterModelCode, new { @class = "control-label" })
                                @Html.ValidationMessageFor(model => model.RouterModelCode, string.Empty, new { @class = "control-label", data_validation_for = $"#{nameof(Model.RouterModelCode)}" })
                                @Html.DropDownListFor(model => model.RouterModelCode, ViewBag.SelectListRouterModels as IEnumerable<SelectListItem>, Misc.SelectOptionalLabel, new { @class = "form-control input-sm", id = $"{nameof(Model.RouterModelCode)}" })
                            </div>
                        </div>
                        <div class="col-sm-4">
                            <div class="form-group-sm">
                                @Html.LabelFor(model => model.Provider, new { @class = "control-label" })
                                @Html.ValidationMessageFor(model => model.Provider, string.Empty, new { @class = "control-label", data_validation_for = $"#{nameof(Model.Provider)}" })
                                @Html.TextBoxFor(model => model.Provider, new { @class = "form-control input-sm", id = $"{nameof(Model.Provider)}" })
                            </div>
                        </div>
                        <div class="col-sm-4">
                            <div class="form-group-sm">
                                @Html.LabelFor(model => model.IsActive, new { @class = "control-label" })
                                @Html.ValidationMessageFor(model => model.IsActive, string.Empty, new { @class = "control-label", data_validation_for = $"#{nameof(Model.IsActive)}" })
                                @Html.DropdownListBooleanFor(model => model.IsActive, FieldNames.Active, FieldNames.Inactive, new { @class = "form-control", id = $"{nameof(Model.IsActive)}" })
                            </div>
                        </div>
                        <div class="col-sm-4">
                            <div class="form-group-sm">
                                @Html.LabelFor(model => model.DataPlan, new { @class = "control-label" })
                                @Html.ValidationMessageFor(model => model.DataPlan, string.Empty, new { @class = "control-label", data_validation_for = $"#{nameof(Model.DataPlan)}" })
                                @Html.TextBoxFor(model => model.DataPlan, new { @class = "form-control input-sm", id = $"{nameof(Model.DataPlan)}" })
                            </div>
                        </div>
                        <div class="col-sm-12">
                            <div class="form-group-sm">
                                @Html.LabelFor(model => model.Remarks, new { @class = "control-label" })
                                @Html.ValidationMessageFor(model => model.Remarks, string.Empty, new { @class = "control-label", data_validation_for = $"#{nameof(Model.Remarks)}" })
                                @Html.TextAreaFor(model => model.Remarks, new { @class = "form-control input-sm", id = $"{nameof(Model.Remarks)}", style = "resize: none" })
                            </div>
                        </div>
                    </div>
                </div>
                <div class="panel-footer">
                    <button type="submit" class="btn btn-sm btn-success"><i class="fa fa-floppy-o"></i>@ComponentNames.Save</button>
                </div>
            </div>
        }
    </div>
</div>
     public class RegisterRouterValidator : AbstractValidator<RegisterRouterViewModel>
    {
        public RegisterRouterValidator()
        {
            RuleFor(x => x.Name).NotEmpty();
            RuleFor(x => x.IsActive).NotNull();
            RuleFor(x => x.Provider).NotEmpty();
            RuleFor(x => x.DataPlan).NotEmpty();
            RuleFor(x => x.Remarks).Length(0, DatabaseValidations.GenericLargeString);
            RuleFor(x => x.MacAdrress).Cascade(CascadeMode.StopOnFirstFailure).NotEmpty().SetValidator(new MacAddressUnicityValidator());
            RuleFor(x => x.ClientId).Cascade(CascadeMode.StopOnFirstFailure).NotEmpty().SetValidator(new ClientExistsValidator());
            RuleFor(x => x.RouterModelCode).Cascade(CascadeMode.StopOnFirstFailure).NotEmpty().SetValidator(new RouterModelExistsValidator());
        }
    }
[Validator(typeof(RegisterRouterValidator))]
    public class RegisterRouterViewModel
    {
        [Display(Name = "MacAdrress", ResourceType = typeof(FieldNames))]
        public string MacAdrress { get; set; }

        [Display(Name = "Name", ResourceType = typeof(FieldNames))]
        public string Name { get; set; }

        [Display(Name = "Provider", ResourceType = typeof(FieldNames))]
        public string Provider { get; set; }

        [Display(Name = "Situation", ResourceType = typeof(FieldNames))]
        public bool IsActive { get; set; }

        [Display(Name = "Remarks", ResourceType = typeof(FieldNames))]
        public string Remarks { get; set; }

        [Display(Name = "DataPlan", ResourceType = typeof(FieldNames))]
        public int DataPlan { get; set; }

        [Display(Name = "RouterModel", ResourceType = typeof(FieldNames))]
        public string RouterModelCode { get; set; }

        [Display(Name = "Client", ResourceType = typeof(FieldNames))]
        public int ClientId { get; set; }
    }
 [HttpPost]
        public ActionResult Register(RegisterRouterViewModel viewModel)
        {
            if (!ModelState.IsValid)
            {
                FillRegisterViewBagData();
                return View(viewModel);
            }

            var router = new RouterFactory(context).Build(viewModel);

            context.Routers.Add(router);
            context.SaveChanges();

            TempData[MessageTypes.ToastrSuccess] = string.Format(ConfirmationMessages.RegisterRouterConfirmation, router.Name);

            return RedirectToAction("Index");
        }
@thiagomajesk
Copy link
Author

I think I've discovered what the problem is...

The only two fields validating are integers, so I presume MVC is trying to set null in both fields hence the error message (I wrongfully thought MVC would bind the default values for the properties of my viewModel, like '0' for integers, 'false' for boolean and so on)

[Display(Name = "Client", ResourceType = typeof(FieldNames))]
public int ClientId { get; set; }
[Display(Name = "DataPlan", ResourceType = typeof(FieldNames))]
public int DataPlan { get; set; }

What I couldn't understand at first is why only the error messages for those two fields were shown and not for the others invalid fields. (When those fields were filled the rest of the messages would appear).
So I concluded that MVC by default will validate inconsistent values (fields that must have default values) in the ViewModel - simple as that!
That's why even without the validator attribute defined in my viewModel MVC displayed the messages.

The behaviour that I think could be improved is to show all validations only once when the model is not valid. But I think its something by design as FluentValidation leverages MVC validation pipeline - then it makes sense that MVC runs its own validations first.

PS: I'm closing this issue, but I think would be nice to keep post for future reference, as this behaviour may not be clear for begginers.

@JeremySkinner
Copy link
Member

Yes, you're correct. This is one of the limitations of MVC's validation infrastructure (that'll hopefully be changed in the next version).

Essentially, MVC expects validation to happen 1 property at a time, whilst FV expects all the properties to be set first and then validation to happen all at once (to properly support cross-property validation etc).

This limitation can mostly be worked around, except in the case of non-nullable value types (int, DateTime etc). In this case, MVC will always add a validation message (and prevent any further validation from occurring) if the property doesn't have a value specified.

To get around this, you must either:

  • Make the properties nullable
  • Ensure that you send a default value in the request

@lock lock bot locked and limited conversation to collaborators Aug 21, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants