hydrostack / hydro

Hydro brings stateful and reactive components to ASP.NET Core without writing JavaScript
https://usehydro.dev
MIT License
693 stars 16 forks source link

Component parameters that are complex objects are not updated when the form submit button is clicked. #33

Closed KnightSwordAG closed 3 months ago

KnightSwordAG commented 3 months ago

This may be a little difficult to explain but hopefully, I'll supply enough code for the problem:

I have the following snippet:

public class EditProfileForm(SessionHandler<SessionState> sessionHandler, 
    IProfileReader profileReader, ILocationReader locationReader,
    IWriter<Profile> writer) : HydroComponent
{

    [Transient]
    public bool ProfileCreated { get; set; }

    [Transient]
    public bool ProfileUpdated { get; set; }

    public EditViewModel? Profile { get; set; }

}

And a very long form supporting the Profile viewModel.

<form method="post">
    @if (Model.ProfileUpdated)
    {
        <div class="alert alert-success alert-dismissible fade show" role="alert">
            Profile updated
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
        </div>
    }
    <input type="hidden" asp-for="Profile.Id" />
    <input type="hidden" asp-for="Profile.NameIdentifier"  />

    <div class="row mb-3">
        <label asp-for="Profile.Email" class="col-md-2 col-form-label text-md-end"></label>
        <div class="col-md-6 col-lg-5 col-xl-4 col-xxl-3">
            <input asp-for="Profile.Email" class="form-control" 
                   readonly="@(!string.IsNullOrEmpty(Model.Profile?.Email) ? "readonly" : string.Empty)">
            <span asp-validation-for="Profile.Email" class="text-danger"></span>
        </div>
    </div>

    <div class="row">
        <div class="col-md-8 col-lg-7 col-xl-6 col-xxl-5 text-end">
            @if (Model.ProfileExists)
            {
                <button type="submit" class="btn btn-primary" 
                        hydro-on:click="@(() => Model.SaveAsync())">
                    Update Profile
                </button>
            }
            else
            {
                <button type="submit" class="btn btn-primary float-end"
                        hydro-on:click="@(() => Model.SaveAsync())">
                    Create Profile
                </button>
            }
        </div>
    </div>
</form>

Problem is here:

    public async Task SaveAsync()
    {
        if (!Validate() || !ProfileCreationValidation())
            return;

        var nameIdentifierClaim = HttpContext.GetClaim(ClaimTypes.NameIdentifier);

        //if (nameIdentifierClaim is null || !IsUserAuthorized(nameIdentifierClaim))
        //{
        //    ModelState.AddModelError(nameof(nameIdentifierClaim), "Claim cannot be null");
        //}

        var profileId = new ProfileId((Profile?.Id).GetValueOrDefault());
        var profile = profileReader
            .FirstOrDefault(p => p.Id == profileId);

        profile = BuildProfile(profile!);
        await writer.SaveAsync(profile)
            .ConfigureAwait(false);

        SetProfileState(ProfileExists);
    }

When you come into this save async method, both the ModelState and the Profile viewModel don't show updates. There are no fields in the ModelState, and the viewModel object is unchanged. So do the component parameter sub properties not survive on post submission, or am I missing something? I'm asking in particular because it seems none of the examples show this use case, and I'm wondering if it's supported.

Thank you.

kjeske commented 3 months ago

Hi!

How Profile property is instantiated? Do you have some code that does it in the Mount method? This property should not be null after mounting.

When it comes to ModelState, the reason might be related to the way how .NET Data Annotation Validators work. They don't validate the complex objects by default. In this case the Profile property is not annotated in any way to say that it should be validated. What you can do is to create your own attribute to say so, here is an example:

using System.ComponentModel.DataAnnotations;
using Hydro;

namespace MyApp;

public class CompositeValidationResult : ValidationResult, ICompositeValidationResult
{
    private readonly List<ValidationResult> _results = new();
    public IEnumerable<ValidationResult> Results => _results;
    public CompositeValidationResult(string errorMessage) : base(errorMessage) { }
    public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames) { }
    protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) { }
    public void AddResult(ValidationResult validationResult) => _results.Add(validationResult);
}

public class ValidateObjectAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var results = new List<ValidationResult>();
        var context = new ValidationContext(value, null, null);

        Validator.TryValidateObject(value, context, results, true);

        if (results.Count == 0)
        {
            return ValidationResult.Success;
        }

        var compositeResults = new CompositeValidationResult($"Validation for {validationContext.DisplayName} failed!");
        results.ForEach(compositeResults.AddResult);
        return compositeResults;
    }
}

Usage:

public class EditProfileForm(SessionHandler<SessionState> sessionHandler, 
    IProfileReader profileReader, ILocationReader locationReader,
    IWriter<Profile> writer) : HydroComponent
{
    [Transient]
    public bool ProfileCreated { get; set; }

    [Transient]
    public bool ProfileUpdated { get; set; }

    [ValidateObject]
    public EditViewModel Profile { get; set; }
}
KnightSwordAG commented 3 months ago

Hi there! Here is my mount call:

    public override async Task MountAsync()
    {
        Profile = await profileReader
            .GetByNameIdentifierAsync<EditViewModel>(Helpers
                .GetNameIdentifier(HttpContext, sessionHandler))
            .ConfigureAwait(false);
    }

Pretty boilerplate, and it could be null, but if that's the case, we want to create a new one anyway. I'll worry about that when I get to it (likely by creating a default object) but in this particular case, it's definitely not null. In fact, the interface elements bind correctly, they just don't update with new values on the save button post.

Also, I'm not sure about your comment regarding how .NET Validators work. Much of this code is adapted from a traditional razor page and the validation properties did fire when they were validated. And the issue isn't so much validation, but rather that the values aren't updated in the submit method.

I'll take a look at your specific suggestions and see if they work, thank you for the update.

image

I added a screenshot to my specific problem. The edited value is in the Description field. It should end in a period (the changed value) but the question mark is still there, and the value is about to be saved in the database, which occurs correctly, but obviously, the value isn't the intended change.

kjeske commented 3 months ago

Could you install the newest version 0.14.2 and check if the issue still remains?

KnightSwordAG commented 3 months ago

I believe it was the newest version, as I had only just started working with hydro that day, June 7th. I can double check when I get into it later today.

EDIT: Oh, my mistake, I wasn't aware there was an update 2 hours ago. Sure, I can try it a little later.

KnightSwordAG commented 3 months ago

So after the update, it didn't immediately work, but I noticed in the examples that forms needed to also have hydro-bind set on input elements. I had tried that earlier thinking it would work, and it didn't, which is what prompted me to put in an issue, since I had run out of options. So long story short, I put in the update, added the hydro-bind elements and it worked correctly. I'm not sure if the update fixed it, or putting the hydro-bind did, or both, but it's working now. Thank you for the update.

kjeske commented 3 months ago

That's good to hear! Thank you for testing and posting the issue 😄

KnightSwordAG commented 3 months ago

That's good to hear! Thank you for testing and posting the issue 😄

Glad to help the cause!