External keys rendering with the ColumnConnection attribute

As explained in the "Columns" section, properties containing external keys are rendered in display mode with their "display values", and in edit mode with either a select containing both key values and display names or with an autocomplete tag that has the same function of selects, but allows an easy selection also with big sets of data.

The list to be displayed in the select, or the way to provide "autocomplete" suggestions are specified by implementing repectively the IDispalyValueItemsProvider or IDispalyValueSuggestionsProvider interfaces.

The IDispalyValueItemsProvider interface

Task<IEnumerable> GetDisplayValuePairs(object x)
An async method that receives a ClaimsPrincipal (User property available in each View) and returns all key/display names pairs that are compatible with the user permissions.
ClientDisplayValueItemsSelector: string
Used with client-providers that generate Html for client frameworks like Angular, Knockout, etc. The ViewModel property containing the array with all key/display names pairs. The exact syntax for specifying the property in the client viewmodel tree depends on the specific framework the provider is targetted to(Angulat, Knockout.js, etc.). If your application uses the defualt sever controls provider this property may simply return null;

The IDispalyValueSuggestionsProvider interface

string GetDisplayValueSuggestionsUrl(IUrlHelper uri);
A method that given a View IUrlHelper returns the Url where to get all suggestions. This url must contain a string token that will be replaced by the partial typed by the user. The string token cannot appear as substring of any other part of the Url, since the final Url is computed with a simple string replace.
DisplayValueSuggestionsUrlToken: string
The token that will be replaced by the partial string typed by the user.

An example implementation

The class below is an example of implenting both interfaces.

public class ProductTypesProvider :
IDispalyValueItemsProvider, IDispalyValueSuggestionsProvider
{
public string ClientDisplayValueItemsSelector => null;

public string DisplayValueSuggestionsUrlToken
{ get; set; } = "_zzz_";

ApplicationDbContext db;
DefaultCRUDRepository<ApplicationDbContext, ProductType> repo;
public ProductTypesProvider(ApplicationDbContext db)
{
repo = DefaultCRUDRepository.Create(db, db.ProductTypes);
}
public async Task<IEnumerable> GetDisplayValuePairs(object x)
{
return (await repo.GetPage<DisplayValue>(null,
m => m.OrderBy(n => n.Display), 1, 100)).Data;
}

public string GetDisplayValueSuggestionsUrl(IUrlHelper uri)
{
return uri.Action("GetTypes", "DetailTest",
new { search = "_zzz_" });
}
}

The [ColumnConnection] attribute

Once IDispalyValueSuggestionsProvider/IDispalyValueItemsProvider implementations have been defined, they may be referenced in the [ConnectionAttribute] that decorates the ViewModel property containing an external key nalue, as in the examples below:

public string TypeName { get; set; }

[ColumnConnection("TypeName", "Display", "Value",
typeof(ProductTypesItemsProvider )
public int? TypeId { get; set; }

 

public string TypeName { get; set; }

[ColumnConnection("TypeName", "Display", "Value",
typeof(ProductTypesSuggestionProvider), 20)]
public int? TypeId { get; set; }

Where "TypeName" is the name of the VieModel property containing the display value corresponding to the key, "Display" the name of the property of the display property in each key/display property item, and Value the name of the key property in each key/display item. The first example refers to a select type rendering, so ProductTypesItemsProvider is a IDispalyValueItemsProvider implementation. The second example refers to an autocomplete type rendering, and ProductTypesSuggestionProvider is a IDispalyValueSuggestionsProvider implementation. The last parameter specifies the maximum number of suggestions. This overload of the constructor accepts also another optional argument, the dataset name, that is the name of the data structure where results received by the server are cached. Autocomplites that specifies the same name share the same cache thus reducing the overall calls to the server. If no dataset name is provided a name that depends just on the the name of the VieModel property containing the display value corresponding to the key is automatically created ("TypeName" in both our example). This dataset name works properly if the View doesn't contain other properties with the same name but a different semantic.

Both ConnectionAttribute constructors overloads have a last optional boolean parameter called queryDisplay whose default value is false. This parameter affects the way the column is rendered in filter forms, namely if set to "true" no select or autocomplete appears in filter forms, but column filter is rendered as a simple text input based on the display names associated to the external keys.

Custom column templates

This subsection is just for the developers that need custom column edit templates for external key columns.

All information needed to render either an autocomplete or a select are contained in the ColumnConnection property of the Column object available in the template. The example below shows how to use the ColumnConnectionInfos object contained in the ColumnConnection property.

@if (col.ColumnConnection != null)
{
    if (col.ColumnConnection is ColumnConnectionInfosStatic)
    {
        var infos = col.ColumnConnection as ColumnConnectionInfosStatic;
        var items = await infos.GetItems(Context.RequestServices, User);
        <label asp-for="@Model" class="control-label">@col.ColumnTitle</label>
        <select asp-for="@Model" asp-items="@(new SelectList(items, infos.ItemsValueProperty, infos.ItemsDisplayProperty))"
                class="@(!string.IsNullOrEmpty(col.InputDetailCssClass) ? col.InputDetailCssClass : "form-control")">
            @if (col.PlaceHolder != null)
            {
                <option value="">@col.PlaceHolder</option>
            }
        </select>
        <span asp-validation-for="@Model" class="text-danger"></span>
    }
    else
    {
        var infos = col.ColumnConnection as ColumnConnectionInfosOnLine;
        var provider = infos.SuggestionsProvider(Context.RequestServices);
        var tagBuilder = generator.GenerateValidationMessage(ViewContext, col.For.ModelExplorer, col.For.Name, null, "span", null);
        tagBuilder.AddCssClass("text-danger");
        <label for="@TagBuilder.CreateSanitizedId(ViewData.TemplateInfo.GetFullHtmlFieldName(infos.DisplayProperty.Name), IdAttributeDotReplacement)" class="control-label">
            @col.ColumnTitle
        </label>
        <autocomplete for-explorer="@ViewData.ModelExplorer.GetExplorerForProperty(col.For.Name)"
                        display-explorer="@ViewData.ModelExplorer.GetExplorerForProperty(infos.DisplayProperty.Name)"
                        for-expression-override="@col.For.Name"
                        display-expression-override="@infos.DisplayProperty.Name"
                        items-display-property="@infos.ItemsDisplayProperty"
                        items-value-property="@infos.ItemsValueProperty"
                        items-url="@provider.GetDisplayValueSuggestionsUrl(Url)"
                        url-token="@provider.DisplayValueSuggestionsUrlToken"
                        dataset-name="@infos.DataSetName"
                        max-results="@infos.MaxResults"
                        class="@(!string.IsNullOrEmpty(col.InputDetailCssClass) ? col.InputDetailCssClass : "form-control")" />

        tagBuilder.WriteTo(ViewContext.Writer, HtmlEncoder);
    }

}

Fork me on GitHub