Go to content

Bulletcode.NET

MVC

As described in the introduction, Bulletcode.NET contains two packages that extend the ASP.NET Core framework: Bulletcode.Web.Core and Bulletcode.Web.Front. The core package focuses on back-end services, and the front package focuses on the front-end. They can be used independently, but they are most often used together.

Applications which use Bulletcode.Web.Core should call the AddCoreMvcServices() extension method of IMvcBuilder:

services
    .AddControllersWithViews()
    .AddCoreMvcServices( environment );

This method registers services related to internationalization, translating validation messages and display name annotations, and error handling. In addition, the following options can be customized by passing a callback function as the second parameter of AddCoreMvcServices():

  • AreasNamespace - specifies the namespace in which areas are defined. The default value is "App.Web.Areas". For example, if a controller is defined in the App.Web.Areas.Administration.Controllers namespace, it is automatically assigned the "Administration" area, without having to use the Area attribute.
  • BinaryContentTypes - configures the content types for which request data can be accessed as binary data. By default, it’s only enabled for application/octet-stream requests. The binary data can be accessed using the FromBody attribute, for example:
[HttpPost]
public async Task<IActionResult> UploadFile( [FromBody] byte[] data )
{
}

Applications which use Bulletcode.Web.Front should call the AddFrontMvcServices() extension method:

services
    .AddControllersWithViews()
    .AddCoreMvcServices( environment )
    .AddFrontMvcServices();

This method registers services related to assets, the ClientView architecture, session management, and other services described later in this chapter. The ClientView configuration options can be customized by passing a callback function to this method.

Models

Bulletcode.NET uses the Entity Framework for accessing the database. It provides a base database schema, which includes users and events, and can be extended by the application. It also implements services which can be used to access and manipulate these entities. See the Database chapter for more information.

In addition, Bulletcode.NET implements additional validation attributes, which can be used for model validation, and provides a mechanism for translating data annotations, including validation messages and display names of model properties. See the Validation chapter for more information.

The BindScenario attribute makes it possible to implement different validation rules for a model depending on the scenario in which it’s used. For example, entering a password may be required when adding a new user, but optional when editing an existing user. Instead of creating separate models, the Scenario property can be used:

class UserViewModel
{
    public const string Add = nameof( Add );
    public const string Edit = nameof( Edit );

    [BindScenario]
    public string Scenario { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [RequiredIf( IsAddScenario ) )]
    public string Password { get; set; }

    public bool IsAddScenario => Scenario == Add;
}

The model binder for the property decorated with the BindScenario attribute sets its value to the name of the currently executed action, which is usually the same as the name of the controller’s method. If the controller contains a public, static method called GetScenario() or GetScenarioAsync(), that method is called to determine the name of the scenario. The current action name and the action context are passed to this method. For example:

public static string GetScenario( string action, ActionContext context )
{
    if ( action == UserViewModel.Edit
        && int.TryParse( (string)context.RouteData.Values[ "id" ], out int id ) )
    {
        if ( id != context.HttpContext.User.FindUserId() )
            return UserViewModel.EditOtherUser;
    }
    return action;
}

The method above changes the scenario to EditOtherUser when the ID of the edited user is different than the ID of the current user, which makes it possible to implement different validation rules when the administrators edit other users than when they edit themselves.

When a controller’s action creates a new instance of the model, it should properly initialize the Scenario property to make sure that client-side validation rules work correctly. For example:

public async Task<IActionResult> Add()
{
    var model = new UserViewModel
    {
        Scenario = UserViewModel.Add,
    };

    return View( model );
}

Bulletcode.NET also provides an extended version of the ModelState.AddModelError(), which is used when a controller performs additional model validation, where the key is automatically determined using an expression accessing the model’s property:

if ( await _userFormService.IsDuplicateEmailAsync( model, user ) )
    ModelState.AddModelError( model, m => m.Email, _translator._( "User already exists." ) );

Controllers

Controllers in web applications, using both server-side rendered views and client-side views, should inherit the BaseController class defined in Bulletcode.Web.Front.

The BaseController has the AutoValidateAntiforgeryToken attribute, which means that validation of antiforgery tokens is applied to all actions using unsafe HTTP methods, other than GET. See the Security chapter for more information.

The BaseController has an AssetManager property for accessing the IAssetManager service; see Assets for more information.

The FlashSuccess(), FlashWarning() and FlashError() methods can be used to add alert messages to the temporary data, so that they are preserved and displayed after a redirect. See Session for more information.

Actions of the controller can be decorated with the [SaveReturnUrl]. This is especially useful for actions which display a list of object with paging, sorting and filtering using query string parameters. When such action is executed, the last query string passed to it is saved in the user’s session.

The ReturnToAction() method redirects to the specified action of the current controller. If that action is decorated with SaveReturnUrl, the previously remembered query string is restored from the session, and if necessary, merged with route values passed to the ReturnToAction() method. This way, the last page, sort order and filter criteria will be preserved after performing an action, such as deleting an object, for example:

[ReturnToAction]
public async Task<IActionResult> Index()
{
    // retrieve data

    return View( model );
}

[HttpPost]
public async Task<IActionResult> Delete()
{
    // perform action

    return ReturnToAction( "Index" );
}

The return URL can also be obtained without performing a redirect, by calling the ReturnUrl() extension method of IUrlHelper. It’s useful, for example, when a form contains a Cancel button which makes it possible to return to the previous page and preserve the last query string:

<a class="button" href="@Url.ReturnUrl( "Index" )">@_( "Cancel" )</a>

Finally, BaseController contains methods which make it possible to return client-side rendered views. See ClientView for more information.

Controllers which implement REST API methods should inherit the BaseApiController class defined in Bulletcode.Web.Core. It includes the ApiController attribute, and modifies the behavior of the ValidationProblem() method so that the BadRequest response matches the error handling standard described in the Diagnostics chapter.

Views

All views should inherit the BaseRazorPage class defined in Bulletcode.Web.Front. In order to do that globally, create a _ViewImports.cshtml file and add the following directive:

@inherits Bulletcode.Web.Front.Razor.BaseRazorPage<TModel>

You can also use this file to add commonly used namespaces and register tag helpers, for example:

@using Bulletcode.Common.Models
@using Bulletcode.Web.Core.Models
@using Bulletcode.Web.Core.Data.Models
@using Bulletcode.Web.Core.Data.Services
@using Bulletcode.Web.Core.Mvc
@using Bulletcode.Web.Front.Models
@using Bulletcode.Web.Front.Mvc
@using Microsoft.AspNetCore.Authorization

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Bulletcode.Web.Front

In addition to standard ASP.NET tag helpers, Bulletcode.NET implements additional ones which are described in the Tag Helpers chapter. The framework also provides some built-in View Components.

The BaseRazorPage class provides access to the ITranslator service, described in the Internationalization chapter, and to make translating web pages even easier, directly implements the translations methods: _, _n, _p and _pn, so they are available globally inside .cshtml files.

The AssetManager and AssetRenderer properties make it possible to use the asset manager and asset renderer services, described in the Assets chapter.

The RootTags property can be used to access two root HTML tags used in all web pages: <html> and <body>. The root page layout should use them to render the opening and closing tags, for example:

<!DOCTYPE html>
@RootTags.Html.RenderStartTag()
<head>
    @* place head tags here *@
</head>
@RootTags.Body.RenderStartTag()
@RenderBody()
@RootTags.Body.RenderEndTag()
@RootTags.Html.RenderEndTag()

By default, the <html> tag contains the lang attribute, based on the current language, and a font size class which affects the global font size on the page. The ApplicationHeader view component contains a button which can be used to change the font size and remember it by creating a cookie. See View Components for more information.

Views can use the Html and Body tags to add custom classes and other attributes, for example:

@{
    RootTags.Body.AddCssClass( "is-home-page" );
}

Custom view components should inherit the BaseViewComponent class. It provides the AssetManager property which makes it possible to register custom JavaScript code. See View Components for more information.

The IConstHelper service, which can be injected into a view, makes it possible to create select list items based on constants defined in a class. This is similar to the GetEnumSelectList() method of the IHtmlHelper, but works with constants which are not enums, for example string-based constants:

public abstract class Role
{
    [Display( Name = "User" )]
    public const string User = "user";

    [Display( Name = "Adminsistrator" )]
    public const string Adminsistrator = "admin";
}

Note that the class is abstract, not static, to make it possible to use it in a generic method:

@inject IConstHelper ConstHelper

<select asp-for="Role" asp-items="ConstHelper.GetSelectList<Role>()"></select>

The GetName() method makes it possible to retrieve the display name associated with a single constant, for example:

<div>@ConstHelper.GetName<Role>( user.Role )</div>

Similarly, the GetEnumName() extension method of IHtmlHelper returns the display name associated with an single enum value.

The AppendQueryString() method of IUrlHelper can be used to merge the specified set of query string parameters which the existing query string of the current page. It returns the URL containing the current page path and the merged query string. This is used, for example, to generate links for paging and sorting.

Bulletcode.NET makes it possible to render a Razor view to a string. This is especially useful for rendering emails in HTML format; see the Mail chapter for more information.

In addition to using classic, server-side rendered views, Bulletcode.NET also allows rendering views on the client side, using the ClientView architecture and a library of built-in JavaScript components.