Story Details for articles

Golf Tracker - Ep. 12 - Refactor for Tees Vertical

kahanu
Author:
Version:
Views:
2984
Date Posted:
8/24/2011 7:38:46 PM
Date Updated:
8/24/2011 7:38:46 PM
Rating:
0/0 votes
Framework:
ASP.NET MVC 2
Platform:
Windows
Programming Language:
C#
Technologies:
Tags:
golf, golf tracker, ASP.NET MVC, tees, refactoring
Demo site:
Home Page:
Share:

Golf Tracker - Refactor for Tees Vertical

Now comes the moment of truth - having to realize this vision of refactoring into a real application.  This image will remind you what I need to do to modify this application.

what to do

I need to add the Tee table to the database and then build all the necessary components to go with it, including the business objects, the dataobjects, data mappers, repositories, services, all the way up to the MVC application and controllers and views.

As I mentioned in the last episode, I've decided to build an API that will handle all the Round calculating functions, and this API will be called from the service layer.  This way the controllers are still only calling the services, and never calling the repositories directly.  It's actually ok to call repositories from the controllers but I like to have the extra bit of abstraction with services or WCF in case further calculations need to be done, etc.

One of the first things I need to do is add the Tee table to the database.

tee table in database

This will store all the information for the various tees for golf courses.  You can see it has a Foreign Key to the Course table with the CourseID.  Now that we have this table I can create the business object.

01.using System;
02.using System.ComponentModel;
03.using System.ComponentModel.DataAnnotations;
04. 
05.namespace GolfTracker.BusinessObjects
06.{
07.    public class Tee
08.    {
09.        [ScaffoldColumn(false)]
10.        public Guid ID { get; set; }
11. 
12.        [Required(ErrorMessage = "Tee Name is required.")]
13.        [DisplayName("Tee Name")]
14.        public string TeeName { get; set; }
15. 
16.        public Guid CourseID { get; set; }
17. 
18.        [Required(ErrorMessage = "Course Rating is required.")]
19.        [DisplayName("Course Rating")]
20.        public decimal CourseRating { get; set; }
21. 
22.        [Required(ErrorMessage = "Slope is required.")]
23.        public int Slope { get; set; }
24. 
25.        [Required(ErrorMessage = "Yardage is required.")]
26.        public int Yardage { get; set; }
27. 
28.        [Required(ErrorMessage = "Par is required.")]
29.        public int Par { get; set; }
30. 
31.        [Required(ErrorMessage = "Tee Gender is required.")]
32.        [DisplayName("Tee Gender")]
33.        public string TeeGender { get; set; }
34. 
35.        public string rowversion { get; set; }
36.    }
37.}

In the DataObjects I create an ITeeRepository interface that will contain a single member.

01.using System;
02.using System.Collections.Generic;
03. 
04.namespace GolfTracker.DataObjects.Interfaces
05.{
06.    public interface ITeeRepository : ICRUDRepository<GolfTracker.BusinessObjects.Tee>
07.    {
08.        List<GolfTracker.BusinessObjects.Tee> GetTeesForCourse(Guid id);
09.    }
10.}

This will return a list of tees for a selected golf course. 

The IRoundRepository interface will also have a single member that just gets a single round for a player.

01.using System;
02.using System.Collections.Generic;
03. 
04.namespace GolfTracker.DataObjects.Interfaces
05.{
06.    public interface IRoundRepository : ICRUDRepository<GolfTracker.BusinessObjects.Round>
07.    {
08.        List<GolfTracker.BusinessObjects.Round> GetByPlayer(Guid id);
09.    }
10.}

This will return all the rounds for the selected player.  This will be displayed on the details page for the player, showing the rounds played and all the statistics.

RoundManagerAPI

The RoundManageAPI is simply a separate project that contains a class that will handle all the calculations that have to do with posting a round or displaying rounds for a player.  Since there are lots of calculations that are necessary for these actions, having them in one place seems the right thing to do.

Here's the entire code for the RoundManager class.

001.using System;
002.using System.Collections.Generic;
003.using System.Linq;
004.using GolfTracker.BusinessObjects;
005.using GolfTracker.DTO;
006.using GolfTracker.DataObjects.Interfaces;
007. 
008.namespace GolfTracker.Api
009.{
010.    public class RoundManager : IRoundManager
011.    {
012.        private IRoundRepository _roundRepository;
013.        private ITeeRepository _teeRepository;
014.        private IPlayerRepository _playerRepository;
015.        private ICourseRepository _courseRepository;
016. 
017.        public RoundManager(IRoundRepository roundRepository, ITeeRepository teeRepository,
018.            IPlayerRepository playerRepository, ICourseRepository courseRepository)
019.        {
020.            this._roundRepository = roundRepository;
021.            this._teeRepository = teeRepository;
022.            this._playerRepository = playerRepository;
023.            this._courseRepository = courseRepository;
024.        }
025. 
026.        #region IRoundManager Members
027. 
028.        public void SaveRound(GolfTracker.BusinessObjects.Round round)
029.        {
030.            GolfTracker.BusinessObjects.Tee tee = _teeRepository.GetById(round.TeeID);
031. 
032.            round.Differential = CalculateDifferential(round.Score, tee.CourseRating, tee.Slope);
033.            round.CourseRatingAndSlope = tee.CourseRating.ToString() + "/" + tee.Slope.ToString();
034. 
035.            _roundRepository.Insert(round);
036.        }
037. 
038.        public GolfTracker.DTO.RoundDto GetHistory(string indexNumber)
039.        {
040.            Player player = _playerRepository.GetByIndexNumber(indexNumber);
041. 
042.            RoundDto dto = new RoundDto();
043.            dto.PlayerName = player.FirstName + " " + player.LastName;
044.            dto.IndexNumber = player.IndexNumber;
045.            dto.ClubName = player.ClubName;
046.            dto.HistoryItems = GetPlayerHistory(player.ID);
047.            dto.HandicapIndex = CalculateHandicapIndex(dto.HistoryItems);
048. 
049.            return dto;
050.        }
051. 
052.        public SuccessDto GetSuccess(Guid roundId)
053.        {
054.            Round round = _roundRepository.GetById(roundId);
055.            Player player = _playerRepository.GetById(round.PlayerID);
056.            Tee tee = _teeRepository.GetById(round.TeeID);
057.            Course course = _courseRepository.GetById(tee.CourseID);
058. 
059.            SuccessDto dto = new SuccessDto();
060.            dto.RoundID = roundId;
061.            dto.PlayerID = player.ID;
062.            dto.PlayerName = player.FirstName + " " + player.LastName;
063.            dto.IndexNumber = player.IndexNumber;
064.            dto.CourseName = course.CourseName;
065.            dto.CourseRating = tee.CourseRating.ToString();
066.            dto.Slope = tee.Slope.ToString();
067.            dto.City = course.City;
068.            dto.State = course.State;
069.            dto.Score = round.Score.ToString();
070.            dto.DatePlayed = round.DatePlayed.ToShortDateString();
071.            dto.Differential = round.Differential.ToString();
072. 
073.            return dto;
074.        }
075. 
076.        #endregion
077. 
078.        #region Private Methods
079. 
080.        /// <summary>
081.        /// Calculate the HandicapIndex for the given differentials.
082.        /// </summary>
083.        /// <param name="items"></param>
084.        /// <returns></returns>
085.        private string CalculateHandicapIndex(List<HistoryItemDto> items)
086.        {
087.            string result = string.Empty;
088. 
089.            if (items.Count == 0)
090.            {
091.                return result;
092.            }
093. 
094.            // Get the lowest differentials of the list
095.            var newItems = items.OrderBy(d => Convert.ToDecimal(d.Differential)).AsEnumerable();
096.            newItems = newItems.Take(TakeValue(newItems.Count()));
097. 
098.            // Get the average of the taken items
099.            var avg = newItems.Average(h => Convert.ToDecimal(h.Differential));
100. 
101.            // Multiply by .96
102.            var plusBonus = avg * 0.96m;
103. 
104.            // Trim after tenths (do not round)
105.            string str = plusBonus.ToString();
106.            int dot = str.IndexOf(".");
107.            if (dot == -1)
108.                throw new ArgumentException("Dot not found.");
109. 
110.            result = str.Substring(0, dot + 2);
111. 
112.            return result;
113.        }
114. 
115. 
116.        /// <summary>
117.        /// Get the player history.
118.        /// </summary>
119.        /// <param name="playerId"></param>
120.        /// <returns></returns>
121.        private List<HistoryItemDto> GetPlayerHistory(Guid playerId)
122.        {
123.            List<HistoryItemDto> historyList = new List<HistoryItemDto>();
124.            HistoryItemDto dto = null;
125. 
126.            var rounds = _roundRepository.GetByPlayer(playerId)
127.                .OrderByDescending(r => r.DatePlayed).Take(20);
128. 
129.            foreach (var item in rounds)
130.            {
131.                dto = new HistoryItemDto();
132.                dto.DatePlayed = item.DatePlayed.ToShortDateString();
133.                dto.Score = item.Score.ToString();
134.                dto.CourseRatingAndSlope = item.CourseRatingAndSlope;
135.                dto.Differential = item.Differential.ToString();
136.                historyList.Add(dto);
137.            }
138. 
139.            return historyList;
140.        }
141. 
142.        /// <summary>
143.        /// Calculate the differential for inserting with new scores.
144.        /// </summary>
145.        /// <param name="score"></param>
146.        /// <param name="courseRating"></param>
147.        /// <param name="slope"></param>
148.        /// <returns></returns>
149.        private decimal CalculateDifferential(int score, decimal courseRating, int slope)
150.        {
151.            decimal result = 0.0m;
152. 
153.            if (score <= 0)
154.                throw new ArgumentNullException("Score");
155. 
156.            if (courseRating <= 0.0m)
157.                throw new ArgumentNullException("Course Rating");
158. 
159.            if (slope <= 0)
160.                throw new ArgumentNullException("Slope");
161. 
162.            result = ((score - courseRating) * 113) / slope;
163. 
164.            return Math.Round(result, 1);
165.        }
166. 
167.        /// <summary>
168.        /// Used in CalculateHandicapIndex to determine
169.        /// how many records to take based on the overall
170.        /// record count.
171.        /// </summary>
172.        /// <param name="count"></param>
173.        /// <returns></returns>
174.        private int TakeValue(int count)
175.        {
176.            int result = 0;
177. 
178.            switch (count)
179.            {
180.                case 5:
181.                    result = 1;
182.                    break;
183.                case 6:
184.                    result = 1;
185.                    break;
186.                case 7:
187.                    result = 2;
188.                    break;
189.                case 8:
190.                    result = 2;
191.                    break;
192.                case 9:
193.                    result = 3;
194.                    break;
195.                case 10:
196.                    result = 3;
197.                    break;
198.                case 11:
199.                    result = 4;
200.                    break;
201.                case 12:
202.                    result = 4;
203.                    break;
204.                case 13:
205.                    result = 5;
206.                    break;
207.                case 14:
208.                    result = 5;
209.                    break;
210.                case 15:
211.                    result = 6;
212.                    break;
213.                case 16:
214.                    result = 6;
215.                    break;
216.                case 17:
217.                    result = 7;
218.                    break;
219.                case 18:
220.                    result = 8;
221.                    break;
222.                case 19:
223.                    result = 9;
224.                    break;
225.                case 20:
226.                    result = 10;
227.                    break;
228.                default:
229.                    result = 1;
230.                    break;
231.            }
232. 
233.            return result;
234.        }
235.        #endregion
236. 
237.    }
238.}

One significant thing to notice here is that I've decided to have the types that will be returned to the views will be straight DTOs (data transfer objects).  So this means that I am flattening all values that populate the DTO properties and converting them all to strings.  There is no use for anything other than string values here.

By doing this also lets the view simply display the data instead of possibly having to convert values.

So to produce this view...

player details view

... the round manager would populate and return a RoundDto object.

01.using System.Collections.Generic;
02. 
03.namespace GolfTracker.DTO
04.{
05.    public class RoundDto
06.    {
07.        public string PlayerName { get; set; }
08.        public string IndexNumber { get; set; }
09.        public string HandicapIndex { get; set; }
10.        public string ClubName { get; set; }
11.        public List<HistoryItemDto> HistoryItems { get; set; }
12.    }
13.}

The list of HistoryItems is the information for all the rounds of golf for the selected player.

01.namespace GolfTracker.DTO
02.{
03.    public class HistoryItemDto
04.    {
05.        public string DatePlayed { get; set; }
06.        public string Score { get; set; }
07.        public string CourseRatingAndSlope { get; set; }
08.        public string Differential { get; set; }
09.    }
10.}

Managing Data

In order to manage the data, including the golf courses and tees and adding and managing new players, I built an MVC Area called Dashboard.  But in order to make this work as it would in a real world application I included the ASP.NET Membership system into the application.   This way only logged on Administrators will be able to add players or golf course information.

Players can easily post rounds of golf without logging in, since that isn't sensitive information.

The easy way to get to the Dashboard to create a golf course is to go to the home page of the application and click on the "Create Courses" link in Step 1.

create courses link

This will take you to the log on page.

log on

I've included the username and password above the fields so there's no need to have to remember them.  Once you log in you'll be able to manage any particular section.

dashboard courses list

From here you can create or edit a course, or manage the tees for a course.

dashboard tees list

To edit a Tee click the "Edit" link and you'll see this form.

dashboard edit tee form

Conclusion

There are lots more happening under the covers and to describe it all in the article would involve a lot of reading.  So download the source code from the Golf Tracker Kit and examine the code at your leisure.

I'll wrap up in the next episode and go through in a little more detail what I've done to complete this application.

Stay tuned.

Comments

    No comments yet.

 

User Name:
(Required)
Email:
(Required)