Resources

Until now we have hard coded any static text. KIT has a concept known as resources which gives the ability for site owners to customise this text.

Instead of hard coding text instead we will use the "Text" dictionary to display it. If no resource is defined then it will render the default text.

If you wish to override any of the default text, then you could add a migration which adds the overridden text to the "Resources" table. For example:

Insert.IntoTable("Resources").Row(new { Key = "My Text", Value = "My Overridden Text" });

Note: The key must match the key passed into the “Text” dictionary.

To enable this for our module we will replace all static text accordingly:

First we’ll update the "Products.cshtml" file with the following:

@inject Kit.Security.Services.IAuthorizationService Authorization
@model ISortedPagedList<DemoShop.Shop.ViewModels.Admin.ProductViewModel>
<form method="post" asp-action="Products" asp-all-route-data="@Context.Request.GetQueryValues().ToStringDictionary()">
    <nav id="icons">
        <ul class="nav-responsive">
            <li class="new"><a asp-action="NewProduct">@Text["New"]</a></li>
            @if (Model.TotalCount > 0) {
                <li class="divide"></li>
                <li class="edit"><button name="Command" value="Edit">@Text["Edit"]</button></li>
                if (Authorization.IsAllowed((await Context.GetUserAsync())!, DemoShop.Shop.Permission.ProductsDelete)) {
                    <li class="delete"><button type="button" data-bs-toggle="modal" data-bs-target="#confirm-delete-modal">@Text["Delete"]</button></li>
                }
            }
        </ul>
    </nav>
    <div class="card shadow">
        <div class="card-body">
            @if (Model.TotalCount > 0) {
                <div class="table-responsive">
                    <table class="table table-striped">
                        <colgroup>
                            <col style="width: 35px" />
                        </colgroup>
                        <thead>
                            <tr>
                                <th></th>
                                <th class="sorting@(Model.SortBy == "Name" ? (Model.SortDirection == ListSortDirection.Ascending ? " sorting-asc" : " sorting-desc") : "")"><a asp-action="Products" asp-all-route-data="@Context.Request.GetQueryValues().AddRange(new { sortBy = "Name", sortDirection = Model.SortDirection == ListSortDirection.Ascending && Model.SortBy == "Name" ? ListSortDirection.Descending : ListSortDirection.Ascending }).ToStringDictionary()">@Text["Name"]</a></th>
                                <th class="sorting@(Model.SortBy == "Price" ? (Model.SortDirection == ListSortDirection.Ascending ? " sorting-asc" : " sorting-desc") : "")"><a asp-action="Products" asp-all-route-data="@Context.Request.GetQueryValues().AddRange(new { sortBy = "Price", sortDirection = Model.SortDirection == ListSortDirection.Ascending && Model.SortBy == "Price" ? ListSortDirection.Descending : ListSortDirection.Ascending }).ToStringDictionary()">@Text["Price"]</a></th>
                                <th class="sorting@(Model.SortBy == "DateAdded" ? (Model.SortDirection == ListSortDirection.Ascending ? " sorting-asc" : " sorting-desc") : "")"><a asp-action="Products" asp-all-route-data="@Context.Request.GetQueryValues().AddRange(new { sortBy = "DateAdded", sortDirection = Model.SortDirection == ListSortDirection.Ascending && Model.SortBy == "DateAdded" ? ListSortDirection.Descending : ListSortDirection.Ascending }).ToStringDictionary()">@Text["Date Added"]</a></th>
                            </tr>
                        </thead>
                        <tbody>
                            @for (var i = 0; i < Model.Count; i++) {
                                <tr>
                                    <td class="text-center"><input type="radio" name="Id" value="@Model[i].Id"@(i == 0 ? " checked=\"checked\"" : "") class="form-check-input" /></td>
                                    <td>@Format(Model[i].Name)</td>
                                    <td>@Format(Model[i].Price.ToString("c"))</td>
                                    <td>@Format(Model[i].DateAdded.ToFormattedString(true))</td>
                                </tr>
                            }
                        </tbody>
                    </table>
                </div>
            } else {
                @Text["No {0} found.", Text["products"]]
            }
        </div>
        @if (Model.TotalCount > 0) {
            <div class="card-footer">
                <pager model="@Model" />
            </div>
        }
    </div>
    @if (Model.TotalCount > 0) {
        <div id="confirm-delete-modal" class="modal fade">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">@Text["Confirm Delete"]</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                    </div>
                    <div class="modal-body">
                        @Text["Are you sure you want to delete the selected {0}?", Text["product"]]
                    </div>
                    <div class="modal-footer">
                        <button name="Command" value="Delete" class="btn btn-danger" data-bs-dismiss="modal">@Text["Delete"]</button>
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">@Text["Cancel"]</button>
                    </div>
                </div>
            </div>
        </div>
    }
</form>

Note: Now instead of hard coding the text we use the "Text" dictionary e.g. @Text["New"].

We'll also go back to our "NewProduct.cshtml" file and replace it with the following:

@model DemoShop.Shop.ViewModels.Admin.ProductFormViewModel
<form method="post" asp-action="NewProduct">
    <nav id="icons">
        <ul class="nav-responsive">
            <li class="save"><button name="Command" value="Save">@Text["Save"]</button></li>
            <li class="cancel"><a asp-action="Products">@Text["Cancel"]</a></li>
        </ul>
    </nav>
    <div asp-validation-summary="All"></div>
    <div class="card shadow">
        <div class="card-header"><h2>@Text["Details"]</h2></div>
        <div class="card-body">
            <div class="form-group">
                <label asp-for="Name" class="form-label"></label>
                <input asp-for="Name" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Price" class="form-label"></label>
                <input asp-for="Price" class="form-control" />
            </div>
        </div>
    </div>
</form>

Note: The label tag will treat the text as a resource. You can still override the name's label text by adding a resource with the key "Name". If this is too ambiguous then you can set the type name with the container type's name e.g. "DemoShop.Shop.ViewModels.Admin.ProductFormViewModel".

And also replace the “EditProduct.cshtml" file with the following:

@model DemoShop.Shop.ViewModels.Admin.ProductFormViewModel
<form method="post" asp-action="EditProduct" asp-route-id="@Model.Id" asp-route-returnurl="@Context.Request.Query.Get("ReturnUrl")">
    <nav id="icons">
        <ul class="nav-responsive">
            <li class="save"><button name="Command" value="Save">@Text["Save"]</button></li>
            <li class="cancel"><a href="@Format(Context.Request.Query.Get("ReturnUrl") ?? Url.Action("Products"))">@Text["Cancel"]</a></li>
        </ul>
    </nav>
    <div asp-validation-summary="All"></div>
    <div class="card shadow">
        <div class="card-header"><h2>@Text["Details"]</h2></div>
        <div class="card-body">
            <div class="form-group">
                <label asp-for="Name" class="form-label"></label>
                <input asp-for="Name" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Price" class="form-label"></label>
                <input asp-for="Price" class="form-control" />
            </div>
        </div>
    </div>
</form>

Finally we'll go back and update the "AdminController.cs" file and replace it with the following:

using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using DemoShop.Shop.Models;
using DemoShop.Shop.ViewModels.Admin;
using Kit.Data;
using Kit.Security;
using Kit.Security.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace DemoShop.Shop.Controllers;

[Area("DemoShop.Shop"), Route("admin")]
public class AdminController : Controller {
    private readonly IAuthorizationService _authorizationService;
    private readonly IDataContext _dataContext;
    private readonly IStringLocalizer _localizer;

    public AdminController(IAuthorizationService authorizationService, IDataContext dataContext, IStringLocalizer localizer) {
        _authorizationService = authorizationService;
        _dataContext = dataContext;
        _localizer = localizer;
    }

    [HasPermission(Permission.Products), HttpGet("products")]
    public async Task<IActionResult> Products(int page = 1, int pageSize = 10, string? sortBy = null, ListSortDirection? sortDirection = null) {
        var timeZone = await HttpContext.GetTimeZoneAsync();

        return View((await _dataContext.Repository<Product>().All().ToSortedPagedListAsync(page, pageSize, sortBy ?? "Name", sortDirection ?? ListSortDirection.Ascending)).Convert(p => new ProductViewModel(p, timeZone)));
    }

    [HasPermission(Permission.Products), HttpPost("products"), ValidateAntiForgeryToken]
    public async Task<IActionResult> Products(string command, int? id, int page = 1, int pageSize = 10, string? sortBy = null, ListSortDirection? sortDirection = null) {
        if (id.HasValue) {
            switch (command) {
                case "Delete":
                    try {
                        // Make sure they are allowed.
                        if (!_authorizationService.IsAllowed((await HttpContext.GetUserAsync())!, Permission.ProductsDelete))
                            return Unauthorized();

                        // Try to delete the product.
                        await _dataContext.Repository<Product>().DeleteAsync((await _dataContext.Repository<Product>().GetAsync(id))!);
                        await _dataContext.CommitAsync();

                        // Set the result text.
                        TempData.Set("FormResult", _localizer["{0} successfully deleted.", _localizer["Product"]].Value);
                    } catch {
                        // Add the error.
                        ModelState.AddModelError("Error", _localizer["{0} cannot be deleted.", _localizer["Product"]].Value);

                        // Rollback the changes.
                        await _dataContext.RollbackAsync();

                        return await Products(page, pageSize, sortBy, sortDirection);
                    }

                    break;
                case "Edit":
                    return RedirectToAction(nameof(EditProduct), new { id, returnUrl = Url.Action(nameof(Products), new { page, pageSize, sortBy, sortDirection }) });
            }
        }

        return RedirectToAction(nameof(Products), new { page = 1, pageSize, sortBy, sortDirection });
    }

    [HasPermission(Permission.Products), HttpGet("products/new")]
    public IActionResult NewProduct() {
        return View(new ProductFormViewModel(new Product()));
    }

    [HasPermission(Permission.Products), HttpPost("products/new"), ValidateAntiForgeryToken]
    public async Task<IActionResult> NewProduct(ProductFormViewModel model) {
        // Validate the form.
        if (!ModelState.IsValid)
            return View(model.Initialize(new Product()));

        // Insert the product.
        await _dataContext.Repository<Product>().InsertOrUpdateAsync(model.ToModel());
        await _dataContext.CommitAsync();

        // Set the result text.
        TempData.Set("FormResult", _localizer["{0} successfully added.", _localizer["Product"]].Value);

        return RedirectToAction(nameof(Products));
    }

    [HasPermission(Permission.Products), HttpGet("products/{id:int}/edit")]
    public async Task<IActionResult> EditProduct(int id) {
        return View(new ProductFormViewModel((await _dataContext.Repository<Product>().GetAsync(id))!));
    }

    [HasPermission(Permission.Products), HttpPost("products/{id:int}/edit"), ValidateAntiForgeryToken]
    public async Task<IActionResult> EditProduct(int id, ProductFormViewModel model, string? returnUrl = null) {
        var product = (await _dataContext.Repository<Product>().GetAsync(id))!;

        // Validate the form.
        if (!ModelState.IsValid)
            return View(model.Initialize(product));

        // Update the product.
        model.ToModel(product);
        await _dataContext.CommitAsync();

        // Set the result text.
        TempData.Set("FormResult", _localizer["{0} successfully updated.", _localizer["Product"]].Value);

        if (Url.IsLocalUrl(returnUrl))
            return Redirect(returnUrl);
        else
            return RedirectToAction(nameof(Products));
    }
}

Notice how we have injected the “IStringLocalizer”, this allows us to convert our text to resources, similarly to how we did in our views.

Also we have taken this opportunity to set the “TempData” after inserting, updating and deleting a product. The “TempData” lives for the lifetime of the next request. For example if you update a product and click “Save” you will notice it will display the message “Product successfully updated.”. Now if you refresh the page this message will disappear.

Templates »