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.Attributes
007.
{
008.
/// <summary>
009.
/// Validate that the incoming handicap index falls between
010.
/// the maximum and minimum acceptable handicap values. And
011.
/// 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 : ValidationAttribute
018.
{
019.
private
const
string
_defaultErrorMessage
020.
=
"Your index of {0}{1} is invalid. It must be between +{2} and {3}."
;
021.
022.
#region ctor
023.
public
ValidateHandicapIndexAttribute(
024.
string
handicapIndexProperty,
string
isPlusProperty)
025.
:
base
(_defaultErrorMessage)
026.
{
027.
IsPlusProperty = isPlusProperty;
028.
HandicapIndexProperty = handicapIndexProperty;
029.
}
030.
#endregion
031.
032.
#region Properties
033.
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
MinimumIndex
046.
{
047.
get
048.
{
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
MaximumIndex
062.
{
063.
get
064.
{
065.
if
(_maximumIndex ==
null
)
066.
return
-40.4m;
067.
return
(
decimal
)_maximumIndex;
068.
}
069.
set
{ _maximumIndex = value; }
070.
}
071.
072.
#endregion
073.
074.
#region Format Error Message
075.
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.
#endregion
085.
086.
#region IsValid Method
087.
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 decimal
100.
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.
#endregion
133.
}
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.Extensions
02.
{
03.
public
static
class
NumericExtensions
04.
{
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.
try
05.
{
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.