Skip to content

Modal Pages

  • Do reference only application contracts package at razor ui layer.
  • Do inherit from the AbpPageModel page model.
  • Do always group modals under two certain development convention:
    • Create
    • Update

Creating A New Entity

  • Do use related input dto which is available at application contracts and always use Input as property name as a binding property at modal razor ui models.
  • Do create ErrorModelInfo with Error name to direct business exceptions from app services to error page.
  • Do always initialize sub collections for dropdown menu items in the constructor.
  • Do initialize input dto in the OnGet method.
  • Do validate model in the OnPost method.
  • Do return No content in the OnPost method.
  • Do inject application services and localizer as private readonly.

Example create modal model:

public class CreateModalModel : ProjectPlanningPageModel
{
    [BindProperty]
    public CreateProjectInput Input { get; set; } // From application contracts

    public ErrorModelInfo Error { get; set; }

    public List<SelectListItem> BaseModelList { get; set; } // Dropdown menu items

    private readonly IProjectAppService _projectAppService; // Application services for use cases

    private readonly IBaseModelAppService _baseModelAppService;  // Application services for use cases

    public CreateModalModel(IProjectAppService projectAppService, IBaseModelAppService baseModelAppService, IStringLocalizer<ProjectPlanningResource> localizer)
    {
        _projectAppService = projectAppService;
        _baseModelAppService = baseModelAppService;
        _localizer = localizer;

        BaseModelList = new List<SelectListItem>
        {
            new SelectListItem { Value = "", Text = _localizer["Select"].Value }
        };
    }

    public async Task<IActionResult> OnGetAsync()
    {
        Input = new CreateProjectInput();

        Error = new ErrorModelInfo();

        try
        {
            var baseModels = await _baseModelAppService.GetListAsync(new GetBaseModelInput { MaxResultCount = LimitedResultRequestDto.MaxMaxResultCount });

            BaseModelList.AddRange(baseModels.Items.Select(b => new SelectListItem(b.Name, b.Id.ToString())).ToList());
        }
        catch (Exception ex)
        {
            Logger.LogError(ex.Message);
            Error.HasError = true;
            Error.Message = _localizer["Oopst!"].Value;
            return Page();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        ValidateModel();

        await _projectAppService.CreateAsync(Input);

        return NoContent();
    }

    public class ErrorModelInfo
    {
        public bool HasError { get; set; }

        public string Message { get; set; }
    }    
}

The related Input DTO:

public class CreateProjectInput : ExtensibleObject
{
    [Required]
    public string Name { get; set; }

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

    public CreateProjectInput()
        : base(setDefaultsForExtraProperties: false)
    {

    }
}

The related modal cshtml:

@page
@using Microsoft.AspNetCore.Mvc.Localization
@using GridLab.PSSX.ProjectPlanning.Localization
@using GridLab.PSSX.ProjectPlanning.Pages.Shared.ModalErrorComponent
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@using GridLab.PSSX.ProjectPlanning.Web.Pages.ProjectPlanning.Projects
@using System.Globalization
@inject IHtmlLocalizer<ProjectPlanningResource> L
@model CreateModalModel
@{
    Layout = null;
}

<form data-ajaxForm="true" asp-page="/ProjectPlanning/Projects/CreateModal" autocomplete="off">
    <abp-modal id="ProjectCreateModal" size="Large">
        <abp-modal-header title="@L["NewProject"].Value"></abp-modal-header>
        <abp-modal-body>
            @if (Model.Error.HasError)
            {
                @(await Component.InvokeAsync<ModalErrorViewComponent>(new { model = new ModalErrorPageModel { ErrorMessage = Model.Error.Message } }))
            }
            else
            {
                <abp-tabs name="CreateProjectTab">
                    <abp-tab title="@L["GeneralData"].Value">
                        <abp-input asp-for="Input.Name" label="@L["Name"].Value" />
                        <abp-input asp-for="Input.Description" label="@L["Description"].Value" />
                        <abp-input asp-for="Input.Version" label="@L["Version"].Value" />
                        <abp-select asp-items="Model.PriorityList" asp-for="Input.Priority" label="@L["Priority"].Value" />
                        <abp-select asp-items="Model.DefinitionList" asp-for="Input.DefinitionId" label="@L["Definition"].Value" />
                    </abp-tab>
                    <abp-tab title="@L["BaseModel"].Value">
                        <abp-select asp-items="Model.BaseModelList" asp-for="Input.BaseModelId" label="@L["BaseModel"].Value" />
                    </abp-tab>
                    <abp-tab title="@L["Dates"].Value">
                        <abp-input asp-for="Input.StartDate" value="@DateTime.Now.ToString("yyyy-MM-dd")" type="date" label="@L["StartDate"].Value" />
                        <abp-input asp-for="Input.EndDate" value="" type="date" label="@L["EndDate"].Value" />
                        <abp-input asp-for="Input.CommissionDate" value="" type="date" label="@L["CommissionDate"].Value" />
                        <abp-input asp-for="Input.InServiceDate" value="" type="date" label="@L["InServiceDate"].Value" />
                    </abp-tab>
                    <abp-tab title="@L["Projects"].Value">
                        <div class="mb-3">
                            <label class="form-label">@L["Successors"]</label>
                            <select id="successors" asp-for="Input.SuccessorIds" data-placeholder="@L["Select"].Value" multiple></select>
                        </div>
                        <div class="mb-3">
                            <label class="form-label">@L["Predecessors"]</label>
                            <select id="predecessors" asp-for="Input.PredecessorIds" data-placeholder="@L["Select"].Value" multiple></select>
                        </div>
                        @* <select asp-for="Input.SuccessorIds" abp multiple select example
                    label="@L["Successors"].Value"
                    multiple="multiple"
                    class="auto-complete-select"
                    data-autocomplete-api-url="/api/ProjectPlanning/project-management/projects"
                    data-autocomplete-display-property="name"
                    data-autocomplete-value-property="id"
                    data-autocomplete-items-property="items"
                    data-autocomplete-filter-param-name="name"
                    data-autocomplete-allow-clear="true">
                    </select>*@
                    </abp-tab>
                </abp-tabs>
            }
        </abp-modal-body>
        <abp-modal-footer>
            <abp-button id="cancel" text="@L["Cancel"].Value" button-type="Secondary" />
            @if (!Model.Error.HasError)
            {
                <abp-button text="@L["Save"].Value" type="submit" icon="check" button-type="Primary" />
            }          
        </abp-modal-footer>
    </abp-modal>
</form>

Updating An Existing Entity

  • Do use Guid datatype and Id as property name as a binding property at modal razor ui models.
  • Do use the ModelInfo postfix for the existing entity object name (ex: ProjectModelInfo).
  • Do inherit entity model info from the IHasConcurrencyStamp interface. Property must be decorated with [HiddenInput] attribute.
  • Do define parameters to be changed in the created model info.
  • Do create ErrorModelInfo with Error name to direct business exceptions from app services to error page.
  • Do always initialize sub collections for dropdown menu items in the constructor.
  • Do define OnGet method with parameter name Id and data type Guid.
  • Do get entity from persistence layer via application services with id parameter in the OnGet method.
  • Do map input dto to the model info in the OnGet method.
  • Do validate model in the OnPost method.
  • Do return No content in the OnPost method.
  • Do inject application services and localizer as private readonly.

Example edit modal model:

public class EditModalModel : ProjectPlanningPageModel
{
    [BindProperty]
    public Guid Id { get; set; }

    [BindProperty]
    public ProjectModelInfo Project { get; set; }

    public ErrorModelInfo Error { get; set; }

    public List<SelectListItem> PriorityList { get; set; }

    public List<SelectListItem> BaseModelList { get; set; }

    private readonly IProjectAppService _projectAppService;

    private readonly IStringLocalizer<ProjectPlanningResource> _localizer;

    public EditModalModel(IProjectAppService projectAppService, IStringLocalizer<ProjectPlanningResource> localizer)
    {
        _projectAppService = projectAppService;
        _localizer = localizer;

        PriorityList = new List<SelectListItem>
        {
            new SelectListItem { Value = "", Text = _localizer["Select"].Value},
            new SelectListItem { Value = Convert.ToInt32(Priority.High).ToString(), Text = _localizer["Project:Priority:High"].Value},
            new SelectListItem { Value = Convert.ToInt32(Priority.Low).ToString(), Text = _localizer["Project:Priority:Low"].Value},
        };
    }

    public async Task<IActionResult> OnGetAsync(Guid id)
    {
        Error = new ErrorModelInfo();

        try
        {
            var project = await _projectAppService.GetAsync(id);

            Project = ObjectMapper.Map<ProjectDto, ProjectModelInfo>(project);

            Id = id;
        }
        catch (AbpRemoteCallException ex) when (ex.Code == ProjectPlanningErrorCodes.ProjectDoesNotExistException | ex.Code == ProjectPlanningErrorCodes.BaseModelDoesNotExistException)
        {
            Logger.LogWarning(ex.Message);
            Error.HasError = true;
            Error.Message = ex.Message;
            return Page();
        }
        catch (Exception ex)
        {
            Logger.LogError(ex.Message);
            Error.HasError = true;
            Error.Message = _localizer["Oopst!"].Value;
            return Page();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        ValidateModel();

        var input = ObjectMapper.Map<ProjectModelInfo, UpdateProjectInput>(Project);
        await _projectAppService.UpdateAsync(Id, input);

        return NoContent();
    }

    public class ErrorModelInfo
    {
        public bool HasError { get; set; }

        public string Message { get; set; }
    }
}
Only parameters defined in ProjectModelInfo will be changed on the modal screen.

The related Entity ModelInfo:

public class ProjectModelInfo : IHasConcurrencyStamp
{
    [Required]
    public string Name { get; set; }

    public string Description { get; set; }

    [Required]
    public Priority Priority { get; set; }

    public int Version { get; set; }

    public DateTime StartDate { get; set; }

    public DateTime? EndDate { get; set; }

    public DateTime? CommissionDate { get; set; }

    public DateTime? InServiceDate { get; set; }

    [HiddenInput]
    public string ConcurrencyStamp { get; set; }
}

The related modal cshtml:

@page
@using Microsoft.AspNetCore.Mvc.Localization
@using GridLab.PSSX.ProjectPlanning.Localization
@using GridLab.PSSX.ProjectPlanning.Pages.Shared.ModalErrorComponent
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@using GridLab.PSSX.ProjectPlanning.Web.Pages.ProjectPlanning.Projects
@using System.Globalization
@inject IHtmlLocalizer<ProjectPlanningResource> L
@model GridLab.PSSX.ProjectPlanning.Pages.ProjectPlanning.Projects.EditModalModel
@{
    Layout = null;
}

<form data-ajaxForm="true" asp-page="/ProjectPlanning/Projects/EditModal" autocomplete="off">
    <abp-modal id="ProjectEditModal" size="Large">
        <abp-modal-header title="@L["EditProject"].Value"></abp-modal-header>
        <abp-modal-body>
            @if (Model.Error.HasError)
            {
                @(await Component.InvokeAsync<ModalErrorViewComponent>(new { model = new ModalErrorPageModel { ErrorMessage = Model.Error.Message } }))
            }
            else
            {
                <abp-tabs name="EditProjectTab">
                    <abp-tab title="@L["GeneralData"].Value">
                        <abp-input asp-for="Project.Name" label="@L["Name"].Value" />
                        <abp-input asp-for="Project.Description" label="@L["Description"].Value" />
                        <abp-input asp-for="Project.Version" label="@L["Version"].Value" />
                        <abp-select asp-items="Model.PriorityList" asp-for="Project.Priority" label="@L["Priority"].Value" />
                    </abp-tab>
                    <abp-tab title="@L["Dates"].Value">
                        <abp-input asp-for="Project.StartDate" value="@Model.Project.StartDate.ToString("yyyy-MM-dd")" type="date" label="@L["StartDate"].Value" />
                        <abp-input asp-for="Project.EndDate" value="@Model.Project.EndDate?.ToString("yyyy-MM-dd")" type="date" label="@L["EndDate"].Value" />
                        <abp-input asp-for="Project.CommissionDate" value="@Model.Project.CommissionDate?.ToString("yyyy-MM-dd")" type="date" label="@L["CommissionDate"].Value" />
                        <abp-input asp-for="Project.InServiceDate" value="@Model.Project.InServiceDate?.ToString("yyyy-MM-dd")" type="date" label="@L["InServiceDate"].Value" />
                    </abp-tab>
                </abp-tabs>
                <abp-input type="hidden" asp-for="@Model.Id" />
                <abp-input type="hidden" asp-for="Project.ConcurrencyStamp" />
            }
        </abp-modal-body>
        <abp-modal-footer>
            <abp-button id="cancel" text="@L["Cancel"].Value" button-type="Secondary" />
            @if (!Model.Error.HasError)
            {
                <abp-button text="@L["Update"].Value" type="submit" icon="plus" button-type="Primary" />
            }
        </abp-modal-footer>
    </abp-modal>
</form>