0

I am working on a razorpages web program which will collect input data for a simulation, run the simulation, and display the results to the user. Several of the outputs of the simulation (e.g. cash flow) are returned as an IEnumerable representing a time series. The report will display these values in a table, as follows:

<table class="table">
    <thead>
        <tr>
            <th>
                Year
            </th>
            <th>
                @Html.DisplayNameFor(model => model.OutputData.Production)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.OutputData.CashFlow)
            </th>
        </tr>
    </thead>
    <tbody>
        @for (int year = 0; year <= Model.InputData.SystemLifetime; year++)
        {
            <tr>
                <td>
                    @year
                </td>
                <td>
                    @Html.DisplayFor(model => model.OutputData.Production[year])
                </td>
                    <td>
                    @Html.DisplayFor(model => model.OutputData.CashFlow[year])
                </td>
            </tr>
        }
        </tbody>
        </table>

The relevant parts of the OutputData class look like:

    public class OutputData
    {
        public double[] Production { get; set; }
        public double[] CashFlow { get; set; }

        public OutputData (InputData inputData)
        {
            ISimulation mySim = SimulationBuilder.BuildSim (inputData) // set up the simulation using the inputs provided by the user.

            Production = mySim.LifetimeProduction().ToArray();
            CashFlow = mySim.LifetimeCashFlows().ToArray();
        }
    }

All of this works, but the data is unformatted. I would like to use data annotation to format the production and cashflow values.

I tried adding the following data annotations to the property definitions:

        [DisplayName("Annual Production")]
        [DisplayFormat(DataFormatString = "{0:F2}")]
        public double[] Production { get; set; }
        [DisplayName("Cash Flow")]
        [DisplayFormat(DataFormatString = "{0:C2}")]
        public double[] CashFlow { get; set; }

(call this Solution A)

This compiles but, as expected, doesn't actually format the data: the data annotations are being applied to the arrays OutputData.Production and Output.CashFlow rather than to their individual items.

I found the following alternative, involving creating custom structs to hold each 'Production' and 'Cash Flow' value, which works but is very ugly and feels overly complicated:

    public class OutputData
    {

        [DisplayName("Annual Production")]
        public ProductionValue[] Production { get; set; }
        public struct ProductionValue
        {
            [DisplayFormat(DataFormatString = "{0:F2}")]
            public double Value { get; init; }
            public ProductionValue (double value) { Value = value; }
            public ProductionValue() { }
        }

        [DisplayName("Cash Flow")]
        public CashFlowValue[] CashFlow{ get; set; }
        public struct CashFlowValue
        {
            [DisplayFormat(DataFormatString = "{0:C2}")]
            public double Value { get; init; }
            public CashFlowValue(double value) { Value = value; }
            public CashFlowValue() { }
        }

        public OutputData (InputData inputData)
        {
            ISimulation mySim = SimulationBuilder.BuildSim (inputData) // set up the simulation using the inputs provided by the user.

            Production = mySim.LifetimeProduction()
                  .Select(item => new ProductionValue(item))
                  .ToArray();
            CashFlow = mySim.LifetimeCashFlows()
                  .Select(item => new CashFlowValue(item))
                  .ToArray();
        }
    }

(note that I also have to change Report.cshtml to bind to the correct property)

(call this Solution B)

Is there a way to do this that looks more like Solution A?

  • You probably need a DisplayTemplate: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/display-templates?view=aspnetcore-7.0 – Mike Brind Feb 24 '23 at 07:22

1 Answers1

0

To address your question directly, I don't see a way to add data formatting as a data annotation to the model other than your Solution B (or something similar).

To keep the clean data model that you want to maintain in Solution A and avoid the bulkier data model and additional processing of the IEnumerable lists for Solution B, view extensions are an alternate solution. They keep data formatting for views out of the data model.

@Mike pointed to DisplayTemplate as an option. Razor Tag Helpers offer a lot of flexibility. (Perhaps you've already looked into this path but decided on data annotations.) Tag helpers allow for the cleaner data model in Solution A and adds more data formatting options in any Razor page for the same data.

<production>@Model.OutputData.Production[year]</production>
<production format="{0:F5}">@Model.OutputData.Production[year]</production>
<production format="{0:F2}">@Model.OutputData.Production[year]</production>
<cashflow format="{0:C2}">@Model.OutputData.CashFlow[year]</cashflow>

Here's the full example:

using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebApplication1.TagHelpers
{
    // https://stackoverflow.com/questions/42039269/create-custom-html-helper-in-asp-net-core
    public static class HTMLHelpers
    {
        public static IHtmlContent DisplayForProduction(
            this IHtmlHelper htmlHelper, double value)
        {
            return new HtmlString(String.Format("{0:F5}", value));
        }

        public static IHtmlContent DisplayForCashFlow(
            this IHtmlHelper htmlHelper, double value)
        {
            return new HtmlString(String.Format("{0:C2}", value));
        }
    }

    // Use the following attribute with 'Attributes' set
    // to trigger this tag helper when all of the attributes 
    // in the comma-separated string are present.
    //[HtmlTargetElement("production", Attributes = "format")]
    public class ProductionTagHelper : TagHelper
    {
        public override async Task ProcessAsync(
            TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.TryGetAttribute("format", 
                out TagHelperAttribute formatAttr);
            string? format = formatAttr?.Value?.ToString();
            if (string.IsNullOrEmpty(format)) { format = "{0:F5}"; }
            output.TagName = ""; // Replace <production> with no tag
            TagHelperContent content = await output.GetChildContentAsync();
            _ = Double.TryParse(content.GetContent(), out double value);
            output.Content.SetContent(String.Format(format, value));
        }
    }

    [HtmlTargetElement("cashflow", Attributes = "format")]
    public class CashFlowTagHelper : TagHelper
    {
        public override async Task ProcessAsync(
            TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.TryGetAttribute("format",
                out TagHelperAttribute formatAttr);
            string? format = formatAttr?.Value?.ToString();
            if (string.IsNullOrEmpty(format)) { format = "{0:C2}"; }
            output.TagName = ""; // Replace <cashflow> with no tag
            TagHelperContent content = await output.GetChildContentAsync();
            _ = Double.TryParse(content.GetContent(), out double value);
            output.Content.SetContent(String.Format(format, value));
        }
    }
}

ProductionCashFlow.cshtml

@page

@* Add 'using' statement for custom HTML Helpers *@
@using WebApplication1.TagHelpers

@* Add statements for Razor Tag Helpers. (List in _ViewImports.cshtml as an alternative.) *@
@addTagHelper WebApplication1.TagHelpers.ProductionTagHelper, WebApplication1
@addTagHelper WebApplication1.TagHelpers.CashFlowTagHelper, WebApplication1

@model WebApplication1.Pages.ProductionCashFlowModel

<h5>Razor Tag Helpers</h5>

<table class="table">
    <thead>
        <tr>
            <th>
                Year
            </th>
            <th>
                @Html.DisplayNameFor(model => model.OutputData.Production)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.OutputData.CashFlow)
            </th>
        </tr>
    </thead>
    <tbody>
        @for (int year = 0; year <= 2; year++)
        {
            <tr>
                <td>
                    @year
                </td>
                <td>
                    <production>@Model.OutputData.Production[year]</production>
                    <production format="">@Model.OutputData.Production[year]</production>
                    <production format="{0:F5}">@Model.OutputData.Production[year]</production>
                    <production format="{0:F2}">@Model.OutputData.Production[year]</production>
                </td>
                <td>
                    <cashflow format="{0:C2}">@Model.OutputData.CashFlow[year]</cashflow>
                </td>
            </tr>
        }
    </tbody>
</table>

<h5>Custom HTML Helpers</h5>

<table class="table">
    <thead>
        <tr>
            <th>
                Year
            </th>
            <th>
                @Html.DisplayNameFor(model => model.OutputData.Production)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.OutputData.CashFlow)
            </th>
        </tr>
    </thead>
    <tbody>
        @for (int year = 0; year <= 2; year++)
        {
            <tr>
                <td>
                    @year
                </td>
                <td>
                    @Html.DisplayForProduction(Model.OutputData.Production[year])
                </td>
                <td>
                    @Html.DisplayForCashFlow(Model.OutputData.CashFlow[year])
                </td>
            </tr>
        }
    </tbody>
</table>

The output looks like:

enter image description here

Dave B
  • 1,105
  • 11
  • 17