Golf Tracker - Player Refactor - Part 2
In this episode I physically refactor all the items I stated in the last episode and I build the custom ValidationAttribute class that will validate whether the players index is falls within the prescribed range.If you recall in my last episode I wanted to refactor the Player form from this...
... to this.
Now there's a check box to determine whether the player is a "plus" or "minus" handicapper.
The next thing I want to do is make sure that the handicap is actually valid, meaning that it falls within the prescribed range. That range is a low of +8.0 to a high of -40.4. I know this seems a bit opposite to the actual values of the numbers, but for the purposes of handicaps, this is correct. I -24 handicap golfer is a higher handicapper than a +1.2 golfer.
So in order to validate that value of the HandicapIndex, I'll create the custom ValidationAttribute class to handle the calculations.
As a reminder, this is what the player class will look like with the new custom ValidationAttribute class.
You can see the ValidateHandicapIndex attribute on the class. It takes two parameters for the two properties needed for the calculation. In this case they are the HandicapIndex property name and the IsPlus property name.
Here is the complete ValidateHandicapIndexAttribute class.
001.using System;002.using System.ComponentModel;003.using System.ComponentModel.DataAnnotations;004.using System.Globalization;005. 006.namespace GolfTracker.BusinessObjects.Attributes007.{008. /// <summary>009. /// Validate that the incoming handicap index falls between010. /// the maximum and minimum acceptable handicap values. And011. /// also make adjustments if it's a plus handicap.012. /// MaximumIndex default is -40.4.013. /// MinimumIndex default is +8.0.014. /// </summary>015. /// 016. [AttributeUsage(AttributeTargets.Class, AllowMultiple=true, Inherited=true)]017. public class ValidateHandicapIndexAttribute : ValidationAttribute018. {019. private const string _defaultErrorMessage 020. = "Your index of {0}{1} is invalid. It must be between +{2} and {3}.";021. 022. #region ctor023. public ValidateHandicapIndexAttribute(024. string handicapIndexProperty, string isPlusProperty)025. : base(_defaultErrorMessage)026. {027. IsPlusProperty = isPlusProperty;028. HandicapIndexProperty = handicapIndexProperty;029. } 030. #endregion031. 032. #region Properties033. 034. private decimal _incomingIndex = 0.0m;035. private bool _isPlus;036. 037. public string HandicapIndexProperty { get; private set; }038. public string IsPlusProperty { get; private set; }039. 040. private decimal? _minimumIndex;041. 042. /// <summary>043. /// This is the greatest (+) plus index (below par player).044. /// </summary>045. public decimal MinimumIndex046. {047. get048. {049. if (_minimumIndex == null)050. return 8.0m;051. return (decimal)_minimumIndex;052. }053. set { _minimumIndex = value; }054. }055. 056. private decimal? _maximumIndex;057. 058. /// <summary>059. /// This is the greatest (-) minus index (above par player).060. /// </summary>061. public decimal MaximumIndex062. {063. get064. {065. if (_maximumIndex == null)066. return -40.4m;067. return (decimal)_maximumIndex;068. }069. set { _maximumIndex = value; }070. }071. 072. #endregion073. 074. #region Format Error Message075. public override string FormatErrorMessage(string name)076. {077. string plusString = string.Empty;078. if (_isPlus)079. plusString = "+";080. 081. return string.Format(CultureInfo.CurrentCulture, ErrorMessageString,082. plusString, _incomingIndex, MinimumIndex, MaximumIndex);083. } 084. #endregion085. 086. #region IsValid Method087. 088. public override bool IsValid(object value)089. {090. // If null return true since we are not checking 091. // for null values. 092. if (value == null) return false;093. 094. // Pull the property values from the class object.095. PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);096. object plusValue = properties.Find(IsPlusProperty, true).GetValue(value);097. object indexValue = properties.Find(HandicapIndexProperty, true).GetValue(value);098. 099. // Cast the incoming handicap index to a decimal100. if (indexValue == null)101. throw new ArgumentNullException("HandicapIndex");102. 103. _incomingIndex = (decimal)indexValue;104. 105. // If the Plus value is not null,106. // cast it to a boolean.107. if (plusValue == null)108. throw new ArgumentNullException("IsPlus");109. 110. _isPlus = (bool)plusValue;111. 112. // If the user enter a negative value,113. // change it to positive for consistency.114. if (_incomingIndex < 0)115. _incomingIndex = _incomingIndex * -1;116. 117. // If the handicap index is not a plus index,118. // change it to a negative value.119. if (!_isPlus)120. _incomingIndex = _incomingIndex * -1;121. 122. // The incoming index must fall between the 123. // minimum and maximum values, otherwise it's invalid.124. if (_incomingIndex < MaximumIndex || _incomingIndex > MinimumIndex)125. {126. return false;127. }128. 129. return true;130. } 131. 132. #endregion133. }134.}This class can also take the MaximumIndex and MinimumIndex values but if they aren't supplied, the default values are used. The IsValid overridden method does most of the work. On lines 96 and 97 it retrieves the actual values from the selected properties. Once the values are validated that they aren't null, the index value is ensured to be positive for calculation purposes and then they are checked against the handicap range. If it passes then the class returns True, otherwise False.
Once the ValidationAttribute is in place, we can see if it works. But first I want to modify one thing in the PlayerController. I want to make sure that the HandicapIndex is always posted to the database as a positive value. Then I can handle whether to display a handicap as plus or minus. To do this I'll create a simple Extension method that will handle the work.
Here's the extension method.
01.namespace GolfTracker.Core.Extensions02.{03. public static class NumericExtensions04. {05. public static decimal MakePositive(this decimal? source)06. {07. decimal result = source;08. 09. if (result < 0)10. {11. result = result * -1;12. }13. 14. return result;15. }16. }17.}The source coming in is the HandicapIndex decimal value. If it's less that zero, then I multiply it by -1 to get a positive value.
This is how it's used in the controller.
01.[HttpPost]02.public ActionResult Create([Bind(Exclude="Id,rowversion")]Player model)03.{04. try05. {06. // Create new id 07. model.ID = Guid.NewGuid();08. model.HandicapIndex.MakePositive();09. 10. if (!ModelState.IsValid)11. return RedirectToAction("Create");12. 13. service.Insert(model);14. 15. return RedirectToAction("Index");16. }17. catch (Exception)18. {19. return RedirectToAction("Create");20. }21.}You can see I simply append it to the HandicapIndex property of the Player model coming into the method.
Modifying the View
What I want to do now is modify the Index view for the Players to display the Index in a more pleasant way. The existing way it's displayed is as a simple decimal. So a 9 handicapper would be displayed as 0.90. This isn't very helpful especially if this player was a 2 handicapper since it wouldn't be telling us whether he's a plus or minus handicapper.So I'll modify the Index view to show the handicap in a more friendly way. Here's the Index view.
01.<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/GolfTrack.Master" Inherits="System.Web.Mvc.ViewPage<GolfTracker.Mvc.Web.ViewData.PlayerViewData>" %>02. 03.<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">04. <%= Model.SiteTitle %> - <%= Model.PageTitle %>05.</asp:Content>06. 07.<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">08. 09. <h2><%= Model.PageTitle %></h2>10. 11. 12. <% Html.Telerik().Grid(Model.PlayerList)13. .Name("PlayerGrid")14. .DataKeys(key => key.Add(c => c.ID))15. .Columns(column =>16. {17. 18. column.Bound(model => model.FirstName);19. column.Bound(model => model.LastName);20. column.Bound(model => model.ClubName);21. //column.Bound(model => model.IndexNumber);22. column.Template(action =>23. {24. if (action.IsPlus == true)25. {26. Response.Write("+");27. }28. Response.Write(action.HandicapIndex);29. }).Title("Handicap Index").HtmlAttributes(new { align = "center" });30. 31. column.Template(action =>32. {%>33. <%= Html.ActionLink("Details", "Details", new{ id= action.ID}) %>34. <%});35. })36. .Pageable()37. .Sortable()38. .Render(); %> 39. 40.</asp:Content>41. 42.<asp:Content ID="Content3" ContentPlaceHolderID="HeadContent" runat="server">43.</asp:Content>You can see between lines 22 and 29 that I create a new table column that displays the index properly. It will align the number to the right and display a "plus" (+) symbols in front of the number when necessary. The resulting view will look like this.
At this point the Player vertical has been modified to work with all the changes I've wanted to make. But I can already see that things aren't quite right yet. So I know I'll be refactoring even more.
Stay tuned.

