dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.37k stars 9.99k forks source link

Blazor binding not working properly with inheritance and computed properties (?) #56417

Closed Bartmax closed 3 months ago

Bartmax commented 4 months ago

Is there an existing issue for this?

Describe the bug

When doing blazor SSR post to edit form updating the Name property it doesn't get binded correctly on the [POST]

If I use public new string? Property {get;set;} on the parent model it does apply the correct binding.

public class UserCreateViewModel : UserUpdateViewModel
{
    // UNCOMMENTING THIS LINES THE PROPERTIES ARE BINDED CORRECTLY.
    // public new string? Firstname { get; set; }
    // public new string? Lastname { get; set; }

    public string? Name
    {
        get { return $"{Firstname} {Lastname}".Trim(); }
        set
        {
            var index = (value ?? "").IndexOf(' ');
            if (index >= 0)
            {
                Firstname = value?[..index].Trim();
                Lastname = value?[index..].Trim();
            }
            else
            {
                Firstname = value;
            }
        }
    }
}

public class UserUpdateViewModel
{
    [Required, EmailAddress]
    public string? Email { get; set; }
    public string? Firstname { get; set; }
    public string? Lastname { get; set; }
}

<EditForm method="post" Model="@User" OnValidSubmit="@AddUser" FormName="CreateUserForm" Enhance=true>
    <DataAnnotationsValidator />
    <ValidationSummary />
    <fieldset>
        <legend>Add user</legend>
        <div>
            <label>
                Full name<br />
                <InputText @bind-Value="User!.Name" placeholder="First Last" />
                <ValidationMessage For="() => User!.Name"></ValidationMessage>
            </label>
        </div>
        <div>
            <label>
                Email<br />
                <InputText @bind-Value="User!.Email" placeholder="name@example.com" />
            </label>
        </div>
        <hr />
        <div>
            <a class="button" href="/admin/users/"> Cancel</a>
            <button type="submit">Save user</button>
        </div>
    </fieldset>
    @if (!string.IsNullOrWhiteSpace(ErrorMessage))
    {
        <div>
            Error: @ErrorMessage
        </div>
    }

</EditForm>

@code {
    [SupplyParameterFromForm] public UserCreateViewModel? User { get; set; } = new();

    string ErrorMessage { get; set; } = "";

    async Task AddUser()
    {
        if (User?.Email is not null)
        {
            var user = await UserService.GetUser(User.Email);
            if (user is not null)
            {
                ErrorMessage = "User already exists";
            }
            else
            {
                await UserService.Create(User);
                NavigationManager.NavigateTo("/admin/users");
            }
        }
    }
}

Expected Behavior

Binding was applied correctly to properties Firstname and Lastname without resorting to using new to hide inherited property.

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

9.0.100-preview.5.24307.3

Anything else?

No response

MackinnonBuck commented 3 months ago

Thanks for reaching out, @Bartmax.

Could you please clarify what behavior you're seeing and how you're observing that the binding doesn't happen correctly?

danroth27 commented 3 months ago

We null out the properties that aren't set by the form post. The new properties on the derived class just happen to change the order of when that happens. I'm not actually sure why we bother to try to null out the properties that weren't sent. @captainsafia @javiercn Do you remember why?

Bartmax commented 3 months ago

Sure sorry, I'm not sure exactly why, but if the properties Firstname and Lastname doesn't exists on the inherited view model, the values are empty on the post request.

When I fill the form and set Name="first second" Firstname and Lastname is binded as "" (empty string)

If I uncomment the lines :

// public new string Firstname {get;set;}
// public new string Lastname {get;set;} 

Then if I fill the form and set Name="first second"

Then the binding works as intended and now, Firstname is binded with "first" and Lastname is binded with "second"

Because the setter on the name property sets both.

Let me know if that's clearer. I can maybe make a video showcasing the problem (?) would that help?

danroth27 commented 3 months ago

@Bartmax You can exclude the Firstname and Lastname properties from data binding by applying the [IgnoreDataMember] attribute to each of them.

Bartmax commented 3 months ago

@Bartmax You can exclude the Firstname and Lastname properties from data binding by applying the [IgnoreDataMember] attribute to each of them.

Don't believe this has anything to do with this issue. Specially since adding both without ignore works as expected.

Sounds counterintuitive at least.

Bartmax commented 3 months ago

I confirm that doing

public class UserUpdateViewModel
{
    [Required, EmailAddress]
    public string? Email { get; set; }

    [IgnoreDataMember]
    public string? Firstname { get; set; }

    [IgnoreDataMember]
    public string? Lastname { get; set; }
}

does include works (the properties are setted with the values from Name property). which I don't completely understand but I think there's something of how the binding works that you know and i don't :).

Unfortunately this is not what i want, because I want those properties to be included on that viewmodel, is the inherited one that I do not. But if this is expected behavior for you, I'm fine with the "workaround".

Bartmax commented 3 months ago

Oh missed this one:

We null out the properties that aren't set by the form post.

Now everything makes sense, thanks for explaining. I'm also intrigued to know why you null them out.

dotnet-policy-service[bot] commented 3 months ago

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.