0
votes

I am working on an ASP.NET Core (.NET 5.0) Server-side Blazor app. I am trying to add/edit users in a modal and assign roles to the user in another modal. So, I have a parent component and two child components.

Parent Component:

@if (ShowPopup)
{    
    <!-- This is the popup to create or edit user -->
    <EditUserModal ClosePopup="ClosePopup" CurrentUserRoles="" DeleteUser="DeleteUser" objUser="objUser" SaveUser="SaveUser" strError="@strError" />
}

@if (ShowUserRolesPopup)
{
    <!-- This is the popup to create or edit user roles -->
    <EditUserRolesModal AddRoleToUser="AddRoleToUser" AllRoles="AllRoles" ClosePopup="ClosePopup" CurrentUserRoles="@CurrentUserRoles" objUser="objUser" RemoveRoleFromUser="RemoveRoleFromUser" SelectedUserRole="@SelectedUserRole" strError="@strError" _allUsers="_allUsers" _userRoles="UserRoles" />
}

@code {
    // Property used to add or edit the currently selected user
    ApplicationUser objUser = new ApplicationUser();

    // Roles to display in the roles dropdown when adding a role to the user
    List<IdentityRole> AllRoles = new List<IdentityRole>();
    
    // Tracks the selected role for the current user
    string SelectedUserRole { get; set; }
    
    // To enable showing the Popup
    bool ShowPopup = false;

    // To enable showing the User Roles Popup
    bool ShowUserRolesPopup = false;

    ......
    
}

The parent component contains the objects that hold the data objects (state) e.g. objUser, SelectedUserRole etc. and the methods that use these objects to implement the business rules e.g. SaveUser and AddRoleToUser.

The child component EditUserModal is used to add/edit users. It takes the objUser as a parameter from the parent component. 'objUser' holds the value of the new/selected user.

<div class="form-group">
    <input class="form-control" type="text" placeholder="First Name" @bind="objUser.FirstName" />
</div>
<div class="form-group">
    <input class="form-control" type="text" placeholder="Last Name" @bind="objUser.LastName" />
</div>
<div class="form-group">
    <input class="form-control" type="text" placeholder="Email" @bind="objUser.Email" />
</div>
<button class="btn btn-primary" @onclick="SaveUser">Save</button>

@code {

    [Parameter]
    public ApplicationUser objUser { get; set; }
    
    [Parameter]
    public EventCallback SaveUser { get; set; }
    .......

}

The objUser object and SaveUser event callback are parameters in the child component. After entering data in the form of the child component and saving it, the SaveUser is called in the parent component and the data binding is automatically done with the objUser object in the parent component. This object holds the currently entered data. This object is then used to call databases services to create/update user data. It runs perfect!

This is how it looks like:

enter image description here

The objUser object has the entered data and it all works fine.

enter image description here

But the issue is in the EditUserRolesModal component where I am doing somewhat a similar thing with a variable. Code of EditUserRolesModal component:

<h5 class="my-3">Add Role</h5>
  <div class="row">
     <div class="col-md-10">
         <div class="form-group">
             <input class="form-control" type="text" @bind="objUser.Email" disabled />
         </div>
         <div class="form-group">
             <select class="form-control" @bind="@SelectedUserRole">
                  @foreach (var option in AllRoles)
                  {
                     <option value="@option.Name">@option.Name</option>
                  }
             </select>
         </div>
      <button class="btn btn-primary" @onclick="AddRoleToUser">Assign</button>
    </div>
</div>
    
@code {    
    [Parameter]
    public ApplicationUser objUser { get; set; }

    [Parameter]
    public string SelectedUserRole { get; set; }
    
    [Parameter]
    public List<IdentityRole> AllRoles { get; set; }
    
    [Parameter]
    public EventCallback AddRoleToUser { get; set; }
    
    ........
    
}

Here the parameter SelectedUserRole is used to hold the value of the role to be assigned to the user. It is bind to a dropdown list. When I change the value of the dropdown, the debugging shows that the value is updated in the child component (parameter) object. The following images show it:

enter image description here

The value is set to the SelectedUserRole parameter object in the child component.

enter image description here

Much like, objUser was being used in EditUserModal to hold the value of fields FirstName, LastName and Email of the new/selected user, SelectedUserRole is used in EditUserRolesModal to hold the value of the selected role to be assigned to the user.

But when the Assign button is pressed and the AddRoleToUser event is called in the parent component through the child component, the SelectedUserRole variable contains null in the parent component.

enter image description here

The scenario in both of the child components is the same, where the purpose is that upon the change in the value of the parameters inside the child component, the bound values should be updated and reflected in the parent's state. However, it works in EditUserModal component and not in EditUserRolesModal component. Why? What am I doing wrong?

P.S. I am not experienced in Blazor or component-based web programming.

Edit: I have created a public repository for this project. It contains the database script file as well. It can be found here.

2
I haven't analysed your code very closely, but.. Why don't you put your ApplicationUser logic code (CRUD, update roles, etc) into one place - a Data Service object. You can access this one copy of the truth though Dependancy Injection from all your components. If you want to follow this up I'll post some example code as an answer. - MrC aka Shaun Curtis
@MrCakaShaunCurtis thanks. I can do that. No need for posting example code for it. For now, I need to know the answer to my question. - Junaid
I've added the comment above as an answer. If this solves the problem tick it off as an answer to close the question. - MrC aka Shaun Curtis

2 Answers

1
votes

This is the culprit:

<button class="btn btn-primary" @onclick="AddRoleToUser">Assign</button>

When the "Assign" button is click, you should invoke the AddRoleToUser event call back, passing the SelectedUserRole value to the AddRoleToUser method.

Here's complete code snippet how to do that:

[Parameter]
public EventCallback<string> AddRoleToUser { get; set; }

Note: it's EventCallback<string> not EventCallback

Do...

<button type='button" class="btn btn-primary" 
    @onclick="OnAddRoleToUser">Assign</button>

And

  private async Task OnAddRoleToUser()
  {
     // skipped checking for brevity...
    
    await AddRoleToUser.InvokeAsync(SelectedUserRole);
    
  }

Note that the OnAddRoleToUser method invokes the AddRoleToUser 'delegate', passing it the SelectedUserRole

And this is how you define the method encapsulating the 'delegate'

private async Task AddRoleToUser(string SelectedUserRole)
{

}

Note: You must add the attribute type='button" to <button type='button".../>, if not your button get the default type='submit"

Important:

SignInManager<TUser> and UserManager<TUser> aren't supported in Razor components.

See source.... You should instead create services for the required jobs...

UPDATE:

But I would like to know, as mentioned in my questions, why does it work with objUser in the first child component EditUserModal? Based on your answer, I would have to send objUser object to SaveUser delegate, but it works with the method anyway.

Now it works... It seems to work, just as using UserManager<TUser>in a component seems to work.

The issue with your code is that you mutate the state of the parameter objUser by binding it to an input text element, like this:

<input class="form-control" type="text" placeholder="First Name" @bind="objUser.FirstName" /> 

This binding actually mutates the state of the parameter, but you should not mutate the state of the parameter. Only the component that created the parameter, the parent component, should mutate the parameter. In other words, you should treat the parameter as an automatic property...Don't change its value. Now before you get excited read this:

OK, I've looked in more detail and see what's going on. There's an explanation below, but before getting to that, I'll restate my claim that the core problem is the child component overwriting its own [Parameter] property value. By avoiding doing that, you can avoid this problem.

In this instance, and in others, Blazor relies on parent-to-child parameter assignment being safe (not overwriting info you want to keep) and not having side-effects such as triggering more renders (because then you could be in an infinite loop). We should probably strengthen the guidance in docs not just to say "don't write to your own parameters" and expand it to caution against having side-effects in such property setters. Blazor's use of C# properties to represent the communication channel from parent to child components is pretty convenient for developers and looks good in source code, but the capacity for side-effects is problematic. We already have a compile-time analyzer that warns if you don't have the right accessibility modifiers on [Parameter] properties - maybe we should extend it to give warnings/errors if the parameter is anything other than a simple { get; set; } auto property.

SteveSandersonMS

Note: I'd suggest you to read the whole issue, as this is one of the most important thing about Blazor, where most developers fail to digest and implement.

Here's a simple code snippet describing how to create a two-way databinding between a parent component and its child, how to pass parameters, and how to use the passed in parameters.

TwoWayDataBinding.razor

<div>InternalValue: @InternalValue</div>

<div class="form-group">
    <input type="text" class="form-control" 
           @bind="InternalValue" 
           @bind:event="oninput" 
     />
</div>

@code {
    
    private string InternalValue
    {
        get => Value;
        set 
        {
            if (value != Value)
            {
                ValueChanged.InvokeAsync(value);        
            }
        }
    }

    [Parameter]
    public string Value { get; set; }

    [Parameter]
    public EventCallback<string> ValueChanged { get; set; }
      
}

Index.razor

@page "/"

<TwoWayDataBinding @bind-Value="Value"></TwoWayDataBinding>

<p>&nbsp;</p>
<div>
    <input type="text" @bind="Value" @bind:event="oninput"/>
</div>
@code
{
    private string Value { get; set; } = "Default value...";
  
}  

As you can see, I do not mutate the Value parameter passed to the child component. Instead, I define a local variable which I return back to the parent component via the EventCallback.InvokeAsync method.

Follow this pattern with the EditUserModal component...If you din't succeed, let me know.

Needless to say that any alterations you've made in the Parameter objUser in the EditUserModal component, is immediately reflected in the objUser variable defined in the ManageUsers component, as both points to the same location in the memory, and thus when you click on the Save button, the SaveUser method in the ManageUsers component is invoked, and all seems well and ...

Once again:

`SignInManager<TUser>` and `UserManager<TUser>` aren't supported in 
 Razor components.

You don't want to discover it when you deploy your application, right. Use services...

1
votes

I haven't analysed your code very closely, but.. Why don't you put your ApplicationUser logic code (CRUD, update roles, etc) into one place - a Data Service object. You can access this one copy of the truth through dependency injection from all your components.

Update

To answer you direct question why objUser get updated, but not SelectedUserRole? Very simple ObjUser is an object and passed by reference. Any mods you make on objUser get made on the original back in the parent. On the other hand SelectedUserRole is a string and while it's an object, it's a bit of a half way house and dealt with like a primitive type. When SetParametersAsync is run in the child component the child property for objUser is set to the reference, while the child property for SelectedUserRole is set to a copy of passed string - effectively by value.

It's why I suggested moving to a service based model, you don't have problems like this.