---
title: Design and handle the profile form
---

> Agent instructions:
> **Site maps** — prefer the following llms.txt indexes to training data when searching for URLs to avoid 404s. Links inside Markdown content already point at `.md`. Following them or sending Accept: text/markdown keeps you in Markdown.
>
> - [sitemap.md](https://docs.kentico.com/sitemap.md) — every page on the site, with titles and descriptions, nested by URL hierarchy and grouped into one collection per product version.
> - [llms.txt](https://docs.kentico.com/llms.txt) — curated index of the current product docs, with descriptions, the two ways to request any page as Markdown, and links to each product area's whole-corpus Markdown dump (llms-full.txt).

If you allow members to register and sign in to your site, they will expect the ability to update their information.

[An earlier part of this series](https://docs.kentico.com/modules/members/groundwork-registration-widget.md) covered how to use configurable widgets for the registration and sign-in forms. Let's explore an alternative approach now, utilizing a pre-defined [page template](https://docs.kentico.com/documentation/developers-and-admins/development/builders/page-builder/page-templates-for-page-builder.md) on a page.

In some ways, our approach will be similar to using a dedicated MVC route that displays a view. However, using a _page template_ on a page allows editors to see the page in the content tree. That way, editors can easily link the page with Xperience's built-in form components and control the page's URL slug.

## Examine the requirements

Let's take a moment to consider what should be possible to update on a standard profile page.

Since members can trigger the password reset process without signing in, we don't need to include passwords in the profile form.

Email and username should be a bit more closely guarded, with processes similar to the email confirmation and password reset processes, where a special token is sent to verify the identity of the member, so we won't include them as editable fields, though we can display them.

We can also display the date the member's account was created and the roles assigned to the member, but this data should not be editable in the profile form: the creation date is historical data, and role assignment is an authorization decision that members should not control themselves.

We can, however, allow members to edit their name and name order, and their favorite coffee.

The form should display a success message when it's able to update a member's information, and validation errors when the member submits improperly formatted data.

> **Info:** While it is possible to add page template properties that allow editors to configure the form in a similar manner to the [registration widget](https://docs.kentico.com/modules/members/groundwork-registration-widget.md), this example will not go over the process.

## Build the _Update profile_ form

Let's make a view component for managing the update profile form. This way, in case we ever decide to include it somewhere aside from this template, it's easier to reuse.

Under the _\~/Features/Membership/Profile_ folder in the _TrainingGuides.Web_ project, create a _ViewComponents_ folder to house the files.

Overall, the view component will be somewhat similar to the registration form widget, using an AJAX form with a partial view to manage validation and results.

### Define the view model

The `GuidesMemberProfileViewModel` from [earlier in the series](https://docs.kentico.com/modules/members/build-registration-widget.md#set-up-a-view-model) contains the custom fields that we used to expand the built-in _Member_ type, so we can inherit from this class for our update form, adding the extra fields that we need.

Let's take another peek at that class as a refresher.

```csharp title="GuidesMemberProfileViewModel.cs"
using System.ComponentModel.DataAnnotations;

namespace TrainingGuides.Web.Features.Membership.Profile;

public class GuidesMemberProfileViewModel
{
    [DataType(DataType.Text)]
    [MaxLength(50)]
    [Display(Name = "Given name")]
    public string GivenName { get; set; } = string.Empty;

    [DataType(DataType.Text)]
    [MaxLength(50)]
    [Display(Name = "Family name")]
    public string FamilyName { get; set; } = string.Empty;

    [Display(Name = "Family name goes first")]
    public bool FamilyNameFirst { get; set; } = false;

    [DataType(DataType.Text)]
    [MaxLength(100)]
    [Display(Name = "Favorite coffee")]
    public string FavoriteCoffee { get; set; } = string.Empty;
}
```

Back in the _\~/Features/Membership/Profile/ViewComponents_ folder, create a new class called `UpdateProfileViewModel` that inherits from `GuidesMemberProfileViewModel`.

- For form fields that should be displayed but not editable (e.g., `EmailAddress`), add properties decorated with the `Display` attribute.
- For values that aren't part of the form but need to persist through a post return (e.g., `SubmitButtonText`), use the `HiddenInput` attribute like you did in the registration form,.
- For properties only used in the initial setup (e.g., `Title`), or which only apply after a result is returned (e.g., `SuccessMessage`), no attributes are necessary.
- Add a member role indicator so the form can render role badges and benefit summaries for the member's assigned roles.

```csharp title="UpdateProfileViewModel.cs"
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

namespace TrainingGuides.Web.Features.Membership.Profile;

public class UpdateProfileViewModel : GuidesMemberProfileViewModel
{
    private string fullName = string.Empty;

    [Display(Name = "Full name")]
    public string FullName
    {
        get => !string.IsNullOrWhiteSpace(fullName)
            ? fullName
            : FamilyNameFirst
                ? $"{FamilyName} {GivenName}"
                : $"{GivenName} {FamilyName}";
        set => fullName = value ?? string.Empty;
    }

    [Display(Name = "Email address")]
    public string EmailAddress { get; set; } = string.Empty;

    [Display(Name = "User name")]
    public string UserName { get; set; } = string.Empty;

    [Display(Name = "Member since")]
    public DateTime Created { get; set; }

    [HiddenInput]
    public string SubmitButtonText { get; set; } = string.Empty;

    public string Title { get; set; } = string.Empty;

    public string ActionUrl { get; set; } = string.Empty;

    public string SuccessMessage { get; set; } = string.Empty;

    public IReadOnlyCollection<GuidesRoleViewModel> Roles { get; set; } = [];
}
```

### Add a service

Since our view model needs data from multiple sources, such as information about the current member, assigned roles, and the base URL of the site, let's make a separate service to populate it, rather than a static method within a view model.

Add a _Services_ folder in the _Profile_ directory and define a service that assembles a view model for a given member.

```csharp title="IUpdateProfileService.cs"
namespace TrainingGuides.Web.Features.Membership.Profile;

public interface IUpdateProfileService
{
    /// <summary>
    /// Get the view model for the update profile view component.
    /// </summary>
    /// <param name="guidesMember">The member to base the view model on.</param>
    /// <param name="roles">The roles of the member to include in the view model.</param>
    /// <returns>An <see cref="UpdateProfileViewModel"/> based on the values of the <see cref="GuidesMember"/>'s properties.</returns>
    UpdateProfileViewModel GetViewModel(GuidesMember guidesMember, IEnumerable<GuidesRole> roles);
}
```

```csharp title="UpdateProfileService.cs"
using Kentico.Content.Web.Mvc.Routing;
using Microsoft.Extensions.Localization;
using TrainingGuides.Web.Features.Shared.Helpers;
using TrainingGuides.Web.Features.Shared.Services;

namespace TrainingGuides.Web.Features.Membership.Profile;

public class UpdateProfileService : IUpdateProfileService
{
    private readonly IHttpRequestService httpRequestService;
    private readonly IPreferredLanguageRetriever preferredLanguageRetriever;
    private readonly IStringLocalizer<SharedResources> stringLocalizer;

    public UpdateProfileService(IHttpRequestService httpRequestService,
        IPreferredLanguageRetriever preferredLanguageRetriever,
        IStringLocalizer<SharedResources> stringLocalizer)
    {
        this.httpRequestService = httpRequestService;
        this.preferredLanguageRetriever = preferredLanguageRetriever;
        this.stringLocalizer = stringLocalizer;
    }

    /// <inheritdoc/>
    public UpdateProfileViewModel GetViewModel(GuidesMember guidesMember, IEnumerable<GuidesRole> roles)
    {
        var roleViewModels = roles
            .Select(role => new GuidesRoleViewModel
            {
                Name = role.Name ?? string.Empty,
                BadgeLabel = string.IsNullOrWhiteSpace(role.BadgeLabel) ? role.DisplayName : role.BadgeLabel,
                BadgeColor = string.IsNullOrWhiteSpace(role.BadgeColor) ? "#8107c1" : role.BadgeColor,
                BenefitsSummary = role.BenefitsSummary
            })
            .ToArray();

        return new()
        {
            GivenName = guidesMember?.GivenName ?? string.Empty,
            FamilyName = guidesMember?.FamilyName ?? string.Empty,
            FamilyNameFirst = guidesMember?.FamilyNameFirst ?? false,
            FavoriteCoffee = guidesMember?.FavoriteCoffee ?? string.Empty,
            UserName = guidesMember?.UserName ?? string.Empty,
            EmailAddress = guidesMember?.Email ?? string.Empty,
            FullName = guidesMember?.FullName ?? string.Empty,
            Created = guidesMember?.Created ?? DateTime.MinValue,
            Roles = roleViewModels,
            ActionUrl = httpRequestService.GetAbsoluteUrlForPath(ApplicationConstants.UPDATE_PROFILE_ACTION_PATH, true),
            SubmitButtonText = stringLocalizer["Submit"],
            Title = stringLocalizer["Update profile"]
        };
    }
}
```

> **Tip:** Remember to register the service with the DI container.

```csharp title="~/ServiceCollectionExtensions.cs"
...
public static void AddTrainingGuidesServices(this IServiceCollection services)
{
    ...
    services.AddSingleton<IUpdateProfileService, UpdateProfileService>();
    ...
}
...
```

### Create the view component class

Now we can define the main file of our view component for updating the profile of the current member. **Make sure to pre-populate the model with data about the current member**, so the member does not need to manually re-fill all of their information every time.

To improve the editor experience, add functionality to display a dummy member in **Preview** or **Page Builder** view of the profile page. This way, editors who have not signed in as members won't see a broken, empty profile when they visit the page in the Xperience admin UI. In these modes, skip role retrieval and render the profile without role badges.

> **Tip:** You can prevent unauthenticated members from seeing the profile form by setting its page to [require authentication.](https://docs.kentico.com/documentation/business-users/website-content/secure-pages.md)
>
> We'll explore this process in more detail [later in the membership series](https://docs.kentico.com/modules/members/set-up-restricted-pages.md).

```csharp title="UpdateProfileViewComponent.cs"
using Kentico.Content.Web.Mvc;
using Kentico.PageBuilder.Web.Mvc;
using Kentico.Web.Mvc;
using Microsoft.AspNetCore.Mvc;
using TrainingGuides.Web.Features.Membership.Services;

namespace TrainingGuides.Web.Features.Membership.Profile;

public class UpdateProfileViewComponent(
    IMembershipService membershipService,
    IGuidesRoleService guidesRoleService,
    IHttpContextAccessor httpContextAccessor,
    IUpdateProfileService updateProfileService) : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync()
    {
        var httpContext = httpContextAccessor.HttpContext;
        GuidesMember currentMember;

        bool useDummyMember = httpContext.Kentico().PageBuilder().GetMode() != PageBuilderMode.Off || httpContext.Kentico().Preview().Enabled;

        currentMember = useDummyMember
            ? membershipService.DummyMember
            : await membershipService.GetCurrentMember() ?? membershipService.DummyMember;

        var roles = useDummyMember
            ? []
            : await guidesRoleService.GetRoles(currentMember);

        var model = updateProfileService.GetViewModel(currentMember, roles);

        return View("~/Features/Membership/Profile/ViewComponents/UpdateProfile.cshtml", model);
    }
}
```

```csharp title="~/Features/Membership/Services/IMembershipService.cs"
...
/// <summary>
/// Generates a dummy member for display in page builder and preview modes.
/// </summary>
/// <returns> A dummy member</returns>
GuidesMember DummyMember { get; }
...
```

```csharp title="~/Features/Membership/Services/MembershipService.cs"
...
/// <inheritdoc/>
public GuidesMember DummyMember => new()
{
    UserName = "JohnDoe",
    Email = "JohnDoe@localhost.local",
    GivenName = "John",
    FamilyName = "Doe",
    FamilyNameFirst = false,
    FavoriteCoffee = "Latte",
    Enabled = true,
    Created = DateTime.Now,
    Id = 0
};
...
```

### Create views for the view component

Let's start by creating the view returned by the component's `InvokeAsync` method.

Much like with the [registration widget](https://docs.kentico.com/modules/members/groundwork-registration-widget.md), create an AJAX form that posts to a special action, and use a separate partial view for the form fields within the update target element.

```cshtml title="UpdateProfile.cshtml"
@using TrainingGuides.Web.Features.Membership.Profile;
@using TrainingGuides.Web.Features.Shared.Helpers;

@model UpdateProfileViewModel

@{
    // Using a new guid ensures no conflict if, for some reason, multiple instances of the view component are on the same page.
    string formDivId = $"updateProfileForm{Guid.NewGuid()}";
}
<h3>@Model.Title</h3>

@using (Html.AjaxBeginForm(new AjaxOptions
{
    HttpMethod = "POST",
    InsertionMode = InsertionMode.Replace,
    UpdateTargetId = formDivId
}, new { action = Model.ActionUrl }))
{
    <div id="@formDivId" class="px-2">
        <partial name="~/Features/Membership/Profile/ViewComponents/UpdateProfileForm.cshtml" model="Model" />
    </div>
}
```

```csharp title="~/Features/Shared/Helpers/ApplicationConstants.cs"
...
public const string UPDATE_PROFILE_ACTION_PATH = "/MemberManagement/UpdateProfile";
...
```

Now let's add the partial view that the above code expects.

> **Tip:** **Displaying non-editable fields**
>
> For fields that members are not supposed to edit, do not use the input tag helper with the readonly attribute. Render them directly instead.
>
> This way, values such as the full name, which can change based on a form submission, will update when a response comes back from the controller.
>
> Directly rendered values also clearly indicate to members that the fields are not editable.

If there is a success message, meaning the controller is returning the current instance of the view after a successful update, render it to the page.

When the member has assigned roles, make sure to display them.

```cshtml title="UpdateProfileForm.cshtml"
@using TrainingGuides.Web.Features.Membership.Profile;

@model UpdateProfileViewModel

<div class="form-horizontal">
    <div asp-validation-summary="ModelOnly" class="text-danger field-validation-error"></div>

    <input asp-for="SubmitButtonText">

    <div class="form-group">
        <div class="editing-form-label-cell">
            <label asp-for="UserName" class="control-label form-label mt-3"></label>
        </div>
        <div class="ms-2">
            <strong>@Model.UserName</strong>
        </div>
    </div>
    <div class="form-group">
        <div class="editing-form-label-cell">
            <label asp-for="FullName" class="control-label form-label mt-3"></label>
        </div>
        <div class="ms-2">
            <strong>@Model.FullName</strong>
        </div>
    </div>
    <div class="form-group">
        <div class="editing-form-label-cell">
            <label asp-for="EmailAddress" class="control-label form-label mt-3"></label>
        </div>
        <div class="ms-2">
            <strong>@Model.EmailAddress</strong>
        </div>
    </div>
    <div class="form-group">
        <div class="editing-form-label-cell">
            <label asp-for="Created" class="control-label form-label mt-3"></label>
        </div>
        <div class="ms-2">
            <strong>@Model.Created</strong>
        </div>
    </div>
    @if (Model.Roles.Any())
    {
        <div class="form-group">
            <div class="editing-form-value-cell pt-3">
                @foreach (var role in Model.Roles)
                {
                    <div class="mb-3">
                        <span class="badge rounded-pill px-3 py-2" style="background-color: @role.BadgeColor; color: #ffffff;">@role.BadgeLabel</span>
                        @if (!string.IsNullOrWhiteSpace(role.BenefitsSummary))
                        {
                            <div class="mt-2">@role.BenefitsSummary</div>
                        }
                    </div>
                }
            </div>
        </div>
    }
    <div class="form-group">
        <div class="editing-form-label-cell">
            <label asp-for="GivenName" class="control-label form-label mt-3"></label>
        </div>
        <div class="editing-form-value-cell">
            <input asp-for="GivenName" class="form-control" test-id="givenName">
            <span asp-validation-for="GivenName" class="text-danger field-validation-error"></span>
        </div>
    </div>
    <div class="form-group">
        <div class="editing-form-label-cell">
            <label asp-for="FamilyName" class="control-label form-label mt-3"></label>
        </div>
        <div class="editing-form-value-cell">
            <input asp-for="FamilyName" class="form-control" test-id="familyName">
            <span asp-validation-for="FamilyName" class="text-danger field-validation-error"></span>
        </div>
    </div>
    <div class="form-group">
        <div class="editing-form-label-cell">
            <label asp-for="FamilyNameFirst" class="control-label form-label mt-3"></label>
        </div>
        <div class="editing-form-value-cell">
            <input asp-for="FamilyNameFirst" class="form-check" test-id="familyNameFirst">
            <span asp-validation-for="FamilyNameFirst" class="text-danger field-validation-error"></span>
        </div>
    </div>
    <div class="form-group">
        <div class="editing-form-label-cell">
            <label asp-for="FavoriteCoffee" class="control-label form-label mt-3"></label>
        </div>
        <div class="editing-form-value-cell">
            <input asp-for="FavoriteCoffee" class="form-control" test-id="favoriteCoffee">
            <span asp-validation-for="FavoriteCoffee" class="text-danger field-validation-error"></span>
        </div>
    </div>

    <div class="text-center">
        <button id="register" type="submit" class="btn tg-btn-secondary text-uppercase my-4">@Model.SubmitButtonText</button>
    </div>
    @if (!string.IsNullOrEmpty(Model.SuccessMessage))
    {
        <div class="text-center">
            <strong>@Model.SuccessMessage</strong>
        </div>
    }
</div>
```

## Handle the form with a controller action

In the _Controllers_ directory of the _Membership_ folder, add a new controller called `MemberManagementController`.

> **Info:** You may already have this file if you've taken the time to replicate the reset password functionality from the [finished branch](https://github.com/Kentico/xperience-by-kentico-training-guides/tree/finished/src/TrainingGuides.Web/Features/Membership) of the _Training guides_ repository.

Create a POST action using the same path constant as the _UpdateProfile_ view, adding any new functionality that you need to the membership service.

```csharp title="MemberManagementController.cs"
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using TrainingGuides.Web.Features.Membership.Profile;
using TrainingGuides.Web.Features.Membership.Services;
using TrainingGuides.Web.Features.Shared.Helpers;

namespace TrainingGuides.Web.Features.Membership.Controllers;

public class MemberManagementController : Controller
{
    private readonly IMembershipService membershipService;
    private readonly IStringLocalizer<SharedResources> stringLocalizer;

    private const string UPDATE_PROFILE_FORM_VIEW_PATH = "~/Features/Membership/Profile/ViewComponents/UpdateProfileForm.cshtml";

    public MemberManagementController(IMembershipService membershipService,
        IStringLocalizer<SharedResources> stringLocalizer)
    {
        this.membershipService = membershipService;
        this.stringLocalizer = stringLocalizer;
    }

    /// <summary>
    /// Updates a user profile.
    /// </summary>
    /// <param name="model">View model with profile fields to update.</param>
    [HttpPost($"{{{ApplicationConstants.LANGUAGE_KEY}}}{ApplicationConstants.UPDATE_PROFILE_ACTION_PATH}")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UpdateProfile(UpdateProfileViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return PartialView(UPDATE_PROFILE_FORM_VIEW_PATH, model);
        }

        // Get the current member instead of pulling from the model, so that members cannot attempt to change each others information.
        var guidesMember = await membershipService.GetCurrentMember();

        if (guidesMember is not null)
        {
            var result = await membershipService.UpdateMemberProfile(guidesMember, model);

            if (result.Succeeded)
            {
                var newModel = GetNewUpdateProfileViewModel(model,
                    guidesMember,
                    stringLocalizer["Profile updated successfully."]);

                return PartialView(UPDATE_PROFILE_FORM_VIEW_PATH, newModel);
            }
            else
            {
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }
        }
        return PartialView(UPDATE_PROFILE_FORM_VIEW_PATH, model);
    }

    private UpdateProfileViewModel GetNewUpdateProfileViewModel(UpdateProfileViewModel model, GuidesMember guidesMember, string successMessage) =>
        new()
        {
            Title = model.Title,
            EmailAddress = guidesMember.Email ?? string.Empty,
            UserName = guidesMember.UserName ?? string.Empty,
            Created = guidesMember.Created,
            FullName = guidesMember.FullName,
            GivenName = guidesMember.GivenName,
            FamilyName = guidesMember.FamilyName,
            FamilyNameFirst = guidesMember.FamilyNameFirst,
            FavoriteCoffee = guidesMember.FavoriteCoffee,
            SubmitButtonText = model.SubmitButtonText,
            SuccessMessage = successMessage,
        };
}
```

> **Tip:** Make sure to retrieve the currently signed-in member, rather than retrieving the member based on a submitted field. This way, nobody can update a different member's details by forging a request to submit a username or email that is not their own.

```csharp title="~/Features/Membership/Services/IMembershipService.cs"
...
/// <summary>
/// Updates the profile of a member.
/// </summary>
/// <param name="member">Member to update.</param>
/// <param name="updateProfileViewModel">ViewModel with updated fields.</param>
Task<IdentityResult> UpdateMemberProfile(GuidesMember member, UpdateProfileViewModel updateProfileViewModel);
...
```

```csharp title="~/Features/Membership/Services/MembershipService.cs"
...
/// <inheritdoc />
public async Task<IdentityResult> UpdateMemberProfile(GuidesMember guidesMember, UpdateProfileViewModel updateProfileViewModel)
{
    guidesMember.GivenName = updateProfileViewModel.GivenName;
    guidesMember.FamilyName = updateProfileViewModel.FamilyName;
    guidesMember.FamilyNameFirst = updateProfileViewModel.FamilyNameFirst;
    guidesMember.FavoriteCoffee = updateProfileViewModel.FavoriteCoffee;

    SynchronizeContact(guidesMember);

    return await userManager.UpdateAsync(guidesMember);
}
...
```
