Last week I was working on a project using the ASP.NET MVC 4 framework.The client required some of their form fields to have range limits that would generate a visual warning but not prevent a user from submitting data. The tricky part was that they wanted the range limits to be configured in a database table so that the limits could be changed easily on the fly.
Normally I would do this by simply throwing a Data Annotation Range Attribute on my model field and then using a ValidationMessageFor HTML Helper in my view and call it a day, but in this case that solution did not meet the client's requirements. Using the simple approach above would not allow for configurable ranges since attribute properties have to be hard coded.
My next standard approach was to create a custom range checking attribute to use in my model. This attribute would include the logic for pulling the range limits from the database. However, this approach proved to be somewhat flawed since every time a field with this attribute was rendered in a view, an extra database query would be required, and that seemed somewhat excessive.
My first reaction whenever I notice excessive database queries is to see how I can use Output Caching. The ASP.NET MVC framework makes caching very easy to do: since the limit values would only be changing every few days, it seemed like a perfect opportunity to leverage Output Caching. So I ripped out all of the attributes that I had added to my model and came up with a solution that consists of the following pieces:
1) A controller action with caching enabled that retrieves the limits for a specific property. In this case I made a controller with a single action just for this purpose. The controller extends the BaseController class that gives it access to the database access function called GetRangeLimitsForField. The controller action GetFieldRangeLimits chaches its results for five minutes and varies by the field parameter. If you don't set the VaryByParam properly, then the action will end up caching a value for one field and returning that value for all fields. I also created a DataFields enum that is essentially a list of all fields that I would ever need to set validation limits on, and I created a RangeLimitViewModel as well (which is included below the controller code).
public class RangeLimitController : BaseController
{
[OutputCache(Duration = 300, VaryByParam = "field")]
public ActionResult GetFieldRangeLimits(DataFields field)
{
var rangeLimitViewModel = new RangeLimitViewModel() { Field = field };
var rangeLimits = this.GetRangeLimitsForField(field);
if (rangeLimits != null)
{
rangeLimitViewModel.LowerLimit = rangeLimits.LowerLimit;
rangeLimitViewModel.UpperLimit = rangeLimits.UpperLimit;
}
return PartialView("_RangeLimit", rangeLimitViewModel);
}
}
public class RangeLimitViewModel
{
public DataFields Field { get; set; }
public float? UpperLimit { get; set; }
public float? LowerLimit { get; set; }
}
2) A partial view that generates some client-side attributes that store the limits for a field. This partial view uses the RangeLimitViewModel that is created in the GetFieldRangeLimits action.
@model RangeLimitViewModel
data-limit-field="@Model.Field" @if (Model.LowerLimit.HasValue) { <text>data-limit-lower="@Model.LowerLimit.Value"</text> } @if (Model.UpperLimit.HasValue) { <text>data-limit-upper="@Model.UpperLimit.Value"</text> }
3) A few simple JQuery functions that target any text boxes contained within divs that have the range limit attributes and that highlight the text boxes if their value goes out of the specified range.
function configureLimitInputs() {
$("div[data-limit-field]").each(function () {
var fieldName = $(this).attr("data-limit-field");
var lowerLimit = $(this).attr("data-limit-lower");
var upperLimit = $(this).attr("data-limit-upper");
if (lowerLimit !== undefined || upperLimit !== undefined) {
$("input[type='text']", $(this)).each(function () {
checkIfInputIsInRange(this, fieldName, lowerLimit, upperLimit);
});
$("input[type='text']", $(this)).keyup(function () {
checkIfInputIsInRange(this, fieldName, lowerLimit, upperLimit);
});
}
});
}
function checkIfInputIsInRange(inputBox, fieldName, lowerLimit, upperLimit) {
var currentValue = $(inputBox).val();
var numberValue = parseFloat(currentValue);
if (currentValue === "" || numberValue === NaN) {
$(inputBox).removeClass("out-of-range");
} else {
if ((lowerLimit !== undefined && numberValue < lowerLimit) ||
(upperLimit !== undefined && numberValue > upperLimit)) {
$(inputBox).addClass("out-of-range");
} else {
$(inputBox).removeClass("out-of-range");
}
}
}
4) And a CSS class to highlight a text box with a red border.
.out-of-range, .out-of-range:hover
{
border-color: red;
}
And now you could use the functionality like this in a view:
<div @{ Html.RenderAction("GetFieldRangeLimits", "RangeLimit", new {field = DataFields.ExampleField}); }>
@Html.EditorFor(model => model.ExampleField)
</div>
<script type="text/javascript">configureLimitInputs();</script>