In the last chapter, I dove into some of the important domain logic for the SmartCA application by covering Submittal Transmittals. In this chapter, I will continue that trend by introducing another important new concept to the domain, the Request for Information (RFI). As you will see, the RFI is similar to a Submittal Transmittal in that they share a lot of the same classes: this will also prompt some refactoring.
Contractors can have many questions throughout a project that may concern documents, construction, materials, and so on. In the old days, these questions were answered with a phone call or an informal conversation with the architect in charge. Nowadays, however, it is necessary to document every request and reply between project contractors and the firm that is running the project, which in this case is Smart Design. This documentation is necessary because significant costs and complications may arise during the question/answer process, and the RFI can be used as a tool to shape the project's direction.
Some of the uses of RFIs do not have cost implications, such as a simple non-change request for more information about something shown in the specifications. They can also be used to let the architect know about an occurrence of something on the job site, or to let the architect know about latent or unknown conditions. The most important rule for an RFI is that it must contain all of the necessary information and not be too brief. If a contractor has a question for the architect, the architect needs to know exactly what the question is so that it may be answered properly.
Each RFI needs to be numbered in the sequence issued, per project. The RFI number is later used as a reference for members of the project when the architect answers the questions or resolves the issues. The RFI is a time-sensitive document, and it must include the date that it was sent, as well as the date that a response is needed. It is important that there are no duplicate RFI numbers per project and that there are no gaps between RFI numbers. RFI numbers can be reused across other projects.
In the SmartCA domain, an RFI contains several important business concepts that must be closely followed. In the next few sections, I will be designing the domain model, determining the RFI Aggregate and its boundaries, and designing the Repository for RFIs.
As stated earlier, the most important parts of the RFI are the Date Received, Date Requested By, Date to Field, Question, and Answer properties. Since these are properties, it is a little bit difficult to model their expected behavior in a diagram. This can be remedied by using a Specification (Evans, Domain-Driven Design, Tackling Complexity in the Heart of Software, 225) class to specify the rules for these properties, and actually make the specification part of the domain. This helps convey to the business domain experts what the intended logic is instead of burying it inside of the Request for Information class.
Figure 6.1 shows a drawing showing the relationships among the classes that combine to make up a Request for Information.
Obviously, the root of this aggregate is the Request for Information class. Note the relationships to the Question/Answer Specification, Date to Field Specification, and RFI Number Specification. These relationships make it very clear to the domain experts that there are rules being modeled for these important concepts.
The relationship to the Status class shows exactly what state the RFI is in, such as completed, pending an architect review, and so on. The relationship to the "From" class represents who the RFI is from, and to go along with who it is from is what Contract is associated with the RFI. The relationship to the Specification Section is not as important for an RFI as it was for a Submittal Transmittal. It is quite possible that the RFI may not require a reference to a Specification Section, as the RFI could be requesting information about something else that may have nothing to do with a Specification Section, such as an incident.
The next important part of the diagram is the RFI's relationship to the Routing Item. This is how Smart Design knows to whom each RFI has been routed for action, and the Discipline of that person, such as architect, engineer, or construction administrator. Just like the Submittal Transmittal Aggregate, there is a Copy To relationship from an RFI which represents the list of Recipients who need to be copied on all correspondence having to do with the RFI.
As you can see from the diagram of the RFI Aggregate in Figure 6.2, there are a lot of moving parts.
As you can see from the diagram of the RFI Aggregate in Figure 6.2, there are a lot of moving parts
Notice how I am starting to make use of some of the other Entities introduced in previous chapters, such as the ProjectContact
class, which is used to represent the To property of the Submittal class, the Recipient property of the RoutingItem class, and the Contact property of the CopyTo class. Also, the ProjectContact
class is used in the RFI's From property to represent the person originating the RFI.
The RequestForInformation class has its own identity and is definitely the root of its own Aggregate (see Figure 6.3). All of the other classes in the diagram, except for ProjectContact, Company, and SpecificationSection, belong to the RFI Aggregate. As shown in earlier chapters, ProjectContact belongs to the Project Aggregate, Company is the root of its own Aggregate, and the SpecificationSection class is part of the Submittal Aggregate.
Since the RequestForInformation class is its own Aggregate root, it will have its own Repository, as shown in Figure 6.4.
Since the RequestForInformation class is its own Aggregate root, it will have its own Repository, as shown in Figure 6.4.
Although the Project Aggregate, Company Aggregate, and Submittal Aggregate are part of the RFI Aggregate, I will not be covering their respective Repositories here because they have already been covered in the previous chapters. I will only be covering the RFI Repository in this chapter.
The IRequestForInformationRepository
interface provides access to instances of RFI Repositories. Here is the IRequestForInformationRepository
interface:
using System; using System.Collections.Generic; using SmartCA.Infrastructure.RepositoryFramework; using SmartCA.Model.Projects; namespace SmartCA.Model.RFI { public interface IRequestForInformationRepository : IRepository<RequestForInformation> { IList<RequestForInformation> FindBy(Project project); } }
Its only unique method, FindBy
, should be called fairly often, as almost all of the time RFIs will only be looked at on a per-project basis.
In this section, I will be writing some unit tests of what I expect of the Submittal Repository implementation. As noted before, these tests will compile correctly, but they will also fail until I write the code for the Repository implementation later on in the Solution section.
Please note that there will be more unit tests in the accompanying code for this chapter, but for brevity's sake I am showing the tests that I think are important here.
The purpose of the FindSubmittalsByProjectTest
method is to validate that the correct number of Submittal
instances have been returned by the Submittal Repository for a given Project.
/// <summary> /// A test for FindBy(Project project) /// </summary> [DeploymentItem("SmartCA.sdf"), TestMethod()] public void FindRfisByProjectTest() { // Get a Project reference Project project = ProjectService.GetProject("5704f6b9-6ffa-444c-9583-35cc340fce2a"); // Find all of the RFI's for the Project IList<RequestForInformation> rfis = this.repository.FindBy(project); // Verify that at least one RFI was returned Assert.IsTrue(rfis.Count > 0); }
This method starts out by getting a Project
instance from the ProjectService
class. It then calls the FindBy
method on the repository to get the list of RFI's for the given Project
instance. The method finishes by checking that the repository returned at least one RequestForInformation
.
The purpose of the AddRfiTest
method is to test adding a new RFI to the RFI Repository:
/// <summary> ///A test for Add(RequestForInformation item) ///</summary> [DeploymentItem("SmartCA.sdf"), TestMethod()] public void AddRfiTest() { // Create a new RequestForInformation Guid projectKey = new Guid("5704f6b9-6ffa-444c-9583-35cc340fce2a"); RequestForInformation rfi = new RequestForInformation(projectKey, 2); IList<ItemStatus> statuses = SubmittalService.GetItemStatuses(); rfi.From = ProjectService.GetProject(projectKey).Contacts[0]; rfi.Status = statuses[0]; rfi.Contractor = CompanyService.GetAllCompanies()[0]; IList<SpecificationSection> specSections =
SubmittalService.GetSpecificationSections(); rfi.SpecSection = specSections[0]; // Add the RFI to the Repository this.repository.Add(rfi); // Commit the transaction this.unitOfWork.Commit(); // Reload the RFI and verify it's number RequestForInformation savedRfi = this.repository.FindBy(rfi.Key); Assert.AreEqual(2, savedRfi.Number); // Clean up this.repository.Remove(savedRfi); this.unitOfWork.Commit(); }
This test is a little more complicated than the last test. It starts out by creating a Project Key value, and then passes the Project Key value as well as an RFI number into the constructor of the RequestForInformation
class. Now that I have an initialized the RequestForInformation
instance, the next step is to set the From
property of the RequestForInformation
instance with a ProjectContact
instance. The next property that needs to be set is the Status
property. The Status
property is set to the value of the first ItemStatus
in the list of all ItemStatus
instances. I then set the Contractor
property with a Company
instance that is retrieved by the CompanyService
class. Last, I get the list of all Specification Sections from the SubmittalService
class and set the RFI's SpecSection
property to the first value in the list of Specification Sections.
The next step is to add the RFI to the repository, and then to commit the transaction by calling the Commit
method on the IUnitOfWork
instance. The Commit
method is important because that method calls back into the RFI Repository to tell it to write the RFI's data to the data store.
Once the RFI has been saved, it is then reloaded and the RFI's Number
property is checked to verify that the Add
and Commit
methods worked properly. The last task that the method needs to perform is to remove the RFI. Removing the RFI that was just created leaves the data store in the same state as it was in before the method started, which is important for the rest of the tests that may depend on a known state of the data store.
The purpose of the UpdateTest
method is to find an RFI and update it with a different DateReceived
property value, and then verify that the change was persisted properly.
/// <summary> ///A test for Updating an RFI ///</summary> [DeploymentItem("SmartCA.sdf"), TestMethod()] public void UpdateRfiTest() { IList<RequestForInformation> rfis = this.repository.FindAll(); // Change the RFI's DateReceived value
DateTime dateReceived = DateTime.Now; rfis[0].DateReceived = dateReceived; // Update the Repository this.repository[rfis[0].Key] = rfis[0]; // Commit the transaction this.unitOfWork.Commit(); // Verify that the change was saved IList<RequestForInformation> refreshedRfis = this.repository.FindAll(); Assert.AreEqual(dateReceived.Date, refreshedRfis[0].DateReceived.Value.Date); }
In this method I start out by getting the entire list of RFIs from the data store. I then proceed to change the DateReceived
property value on the first RFI in the list, and then call the indexer method of the IRequestForInformationRepository
. After the call to the indexer, I then use the IUnitOfWork
interface to commit the transaction. Last, I verify that the change actually made it to the data store by reloading the same RFI and checking to see if its DateReceived
property value is the same calendar date that I just assigned to the RFI earlier in the method.
The purpose of the RemoveRfiTest
method is to test the process of removing an RFI from the data store.
/// <summary> ///A test for Remove(RequestForInformation item) ///</summary> [DeploymentItem("SmartCA.sdf"), TestMethod()] public void RemoveRfiTest() { IList<RequestForInformation> rfis = this.repository.FindAll(); // Remove the RFI from the Repository this.repository.Remove(rfis[0]); // Commit the transaction this.unitOfWork.Commit(); // Verify that there is now one less RFI in the data store IList<RequestForInformation> refreshedRfis = this.repository.FindAll(); Assert.AreEqual(0, refreshedRfis.Count); // Reset the state this.AddRfiTest(); }
The first line of this method should look familiar; I am getting the entire list of RFIs from the data store. I then remove the first RFI in the list from the repository. After removing the RFI from the repository, I then use the IUnitOfWork
interface to commit the transaction. Next, I verify that the change actually made it to the data store by using the repository to find all of the RFI instances and making sure there is now one less RFI than before. Last, I call the AddRfiTest
method to add the RFI I just deleted back into the data store in order to reset the original state of the data store.
Now that I have finished going over the design the RFI domain model, Aggregate, and repository, it's time to do my favorite thing: write some code! In this section I will be implementing these designs, as well as implementing the ViewModel and the View for RFIs.
There are two constructors for the RFI class, and they both take a projectKey
parameter of type System.Object
and a number (integer
) parameter. Every RFI must have a number and belong to a Project, so that is why those arguments are in both constructors.
Again, as in the last chapter, I am using a key value for a Project instead of a full blown Project instance, since I can always get to the Project via the ProjectService
class. The second constructor takes a key argument of type System.Object
, thus following the existing pattern for creating instances of existing Entity classes.
using System; using SmartCA.Infrastructure.DomainBase; using SmartCA.Model.Submittals; using SmartCA.Model.Employees; using SmartCA.Model.Projects; using System.Collections.Generic; using System.Text; using SmartCA.Model.Companies; namespace SmartCA.Model.RFI { public class RequestForInformation : EntityBase { private object projectKey; private int number; private DateTime transmittalDate; private ProjectContact from; private int totalPages; private Delivery deliveryMethod; private string otherDeliveryMethod; private string phaseNumber; private bool reimbursable; private bool final; private List<CopyTo> copyToList; private DateTime? dateReceived; private DateTime? dateRequestedBy; private Company contractor;
private SpecificationSection specSection; private List<RoutingItem> routingItems; private string question; private string description; private string contractorProposedSolution; private bool change; private int cause; private int origin; private ItemStatus status; private DateTime? dateToField; private string shortAnswer; private string longAnswer; private string remarks; private RequestForInformationNumberSpecification numberSpecification; private RequestForInformationDateSpecification dateToFieldSpecification; private RequestForInformationQuestionAnswerSpecification questionAnswerSpecification; public RequestForInformation(object projectKey, int number) : this(null, projectKey, number) { } public RequestForInformation(object key, object projectKey, int number) : base(key) { this.projectKey = projectKey; this.number = number; this.transmittalDate = DateTime.Now; this.from = null; this.totalPages = 1; this.deliveryMethod = Delivery.None; this.otherDeliveryMethod = string.Empty; this.phaseNumber = string.Empty; this.reimbursable = false; this.final = false; this.copyToList = new List<CopyTo>(); this.dateReceived = null; this.dateRequestedBy = null; this.contractor = null; this.specSection = null; this.routingItems = new List<RoutingItem>(); this.question = string.Empty; this.description = string.Empty; this.contractorProposedSolution = string.Empty; this.change = false; this.cause = 0; this.origin = 0; this.status = null; this.dateToField = null; this.shortAnswer = string.Empty; this.longAnswer = string.Empty;
this.remarks = string.Empty; this.numberSpecification = new RequestForInformationNumberSpecification(); this.dateToFieldSpecification = new RequestForInformationDateSpecification(); this.questionAnswerSpecification = new RequestForInformationQuestionAnswerSpecification(); this.Validate(); }
All of the data for the RequestForInformation
class is initialized and validated in the second constructor, which is called by the first constructor.
The properties of the RequestForInformation
class are very similar to those of the Submittal
class, so I am only going to show the differences here. Most of the properties in this class are fairly straightforward.
public DateTime? DateRequestedBy { get { return this.dateRequestedBy; } set { this.dateRequestedBy = value; } } public int DaysLapsed { get { int daysLapsed = 0; if (this.dateReceived.HasValue && this.dateToField.HasValue) { daysLapsed = this.dateToField.Value.Subtract(this.dateReceived.Value).Days; } return daysLapsed; } } public Company Contractor { get { return this.contractor; } set { this.contractor = value; } } public string Question { get { return this.question; } set { this.question = value; } }
public string Description { get { return this.description; } set { this.description = value; } } public string ContractorProposedSolution { get { return this.contractorProposedSolution; } set { this.contractorProposedSolution = value; } } public bool Change { get { return this.change; } set { this.change = value; } } public int Cause { get { return this.cause; } set { this.cause = value; } } public int Origin { get { return this.origin; } set { this.origin = value; } } public string ShortAnswer { get { return this.shortAnswer; } set { this.shortAnswer = value; } } public string LongAnswer { get { return this.longAnswer; } set { this.longAnswer = value; } } public RequestForInformationNumberSpecification NumberSpecification { get { return this.numberSpecification; } } public RequestForInformationDateSpecification DateToFieldSpecification { get { return this.dateToFieldSpecification; } }
public RequestForInformationQuestionAnswerSpecification QuestionAnswerSpecification { get { return this.questionAnswerSpecification; } }
This read-only property represents the difference in time from when the RFI was received to when it was sent to the field.
This property is designed to model the business rules about the proper numbering of RFIs. The NumberSpecification
property is represented by the RequestForInformationNumberSpecification
class. Its only job is to validate that the RFI adheres to the numbering rules, which are, if you remember, that all RFIs must be numbered consecutively within a Project, and there cannot be duplicate RFI numbers within a Project.
using System; using SmartCA.Infrastructure.Specifications; using System.Collections.Generic; using SmartCA.Model.Projects; using System.Linq; namespace SmartCA.Model.RFI { public class RequestForInformationNumberSpecification : Specification<RequestForInformation> { public override bool IsSatisfiedBy(RequestForInformation candidate) { bool isSatisfiedBy = true; // Make sure that the same RFI number has not been used for the // current project, and that there are no gaps between RFI numbers // First get the project associated with the RFI Project project = ProjectService.GetProject(candidate.ProjectKey); // Next get the list of RFIs for the project IList<RequestForInformation> requests = RequestForInformationService.GetRequestsForInformation(project); // Determine if the RFI number has been used before isSatisfiedBy = (requests.Where(rfi => rfi.Number.Equals(candidate.Number)).Count() < 1); // See if the candidate passed the first test if (isSatisfiedBy)
{ // First test passed, now make sure that there are no gaps isSatisfiedBy = (candidate.Number - requests.Max(rfi => rfi.Number) == 1); } return isSatisfiedBy; } } }
This code starts out by getting the list of RFIs for the current Project, which is the Project that is associated with the RFI. Once it has the list of RFIs, it then uses a LINQ query to determine whether the count of RFIs in the list that matches the candidate RFI's Number
property is less than one. If the count is less than one, then the test passes.
The next test is to make sure that the candidate RFI will not introduce any numbering gaps within RFIs of the current Project. This is done with another LINQ query to get the highest RFI number (Max
) in the list; then that number is subtracted from the candidate RFI's Number
property. If the result equals one, then the test passes.
This property is designed to model the business rule about the dates associated with RFIs. The DateToFieldSpecification
property is represented by the RequestForInformationDateSpecification
class. Its only job is to validate that the RFI has both a date received value and a date requested by value.
using System; using SmartCA.Infrastructure.Specifications; namespace SmartCA.Model.RFI { public class RequestForInformationDateSpecification : Specification<RequestForInformation> { public override bool IsSatisfiedBy(RequestForInformation candidate) { // Each RFI must have a date received and a date // that the response is needed return (candidate.DateReceived.HasValue && candidate.DateRequestedBy.HasValue); } } }
This code is much simpler than the first Specification class, as it only needs to perform two simple Boolean
checks for the two dates.
This property is designed to model the business rule question and answer associated with RFIs. The QuestionAnswerSpecification
property is represented by the RequestForInformationQuestionAnswerSpecification
class. Its only job is to validate that the RFI has a question entered and either a short answer or a long answer entered.
using System; using SmartCA.Infrastructure.Specifications; namespace SmartCA.Model.RFI { public class RequestForInformationQuestionAnswerSpecification : Specification<RequestForInformation> { public override bool IsSatisfiedBy(RequestForInformation candidate) { // The RFI must have a question and answer // The answer could be the short answer or // the long answer return (!string.IsNullOrEmpty(candidate.Question) && (!string.IsNullOrEmpty(candidate.ShortAnswer) || !string.IsNullOrEmpty(candidate.LongAnswer))); } } }
This code is also performing Boolean
comparisons by ensuring that the Question
property is valid and that either the ShortAnswer
or the LongAnswer
property is valid.
After going over the IRequestForInformationRepository
interface in the Design section, it is now time to explain how the RequestForInformation
class is actually persisted to and from the data store by the RFI Repository. In this section, I will be writing the code for the RFI Repository.
If you have been following along, you know that the application's Template Method pattern implementation that I have been using in the repositories for getting Entity Root instances, the BuildChildCallbacks
method, must be overridden in the RequestForInformationRepository
.
#region BuildChildCallbacks protected override void BuildChildCallbacks() { this.ChildCallbacks.Add(ProjectFactory.FieldNames.ProjectContactId, this.AppendFrom); this.ChildCallbacks.Add("CopyToList", delegate(RequestForInformation rfi, object childKeyName)
{ this.AppendCopyToList(rfi); }); this.ChildCallbacks.Add("RoutingItems", delegate(RequestForInformation rfi, object childKeyName) { this.AppendRoutingItems(rfi); }); } #endregion
The first entry made in the ChildCallbacks
dictionary is for the AppendFrom
method. Thanks to the ProjectService
class's GetProjectContact
method, this method's code is very simple:
private void AppendFrom(RequestForInformation rfi, object fromProjectContactKey) { rfi.From = ProjectService.GetProjectContact(rfi.ProjectKey, fromProjectContactKey); }
You have probably noticed that the AppendCopyToList
and AppendRoutingItems
callbacks look identical to those from the Submittal Repository. Well, you are right! This signals me that I need to do some refactoring of classes and methods. In order to prevent code duplication, I have identified the "area" that needs refactoring, and that "area" is any code that deals with the transmittal aspect of a Submittal or an RFI. I will cover this refactoring in the next few paragraphs.
The whole reason for needing to do a refactoring was the RFI Repository was just about ready to have the same code as the Submittal Repository, and that code was for handling the CopyTo
list and the RoutingItems
list associated with the RFI. Looking at this a little bit further, it seems as if there is more in common between Submittals and RFIs. They both happen to be a document transmittal, and there is certain data around that transmittal that is common. The first step to refactor this was to put everything they have in common into an interface, and I decided to name this interface the ITransmittal
interface.
using System; using System.Collections.Generic; using SmartCA.Model; namespace SmartCA.Model.Transmittals { public interface ITransmittal { object ProjectKey { get; } DateTime TransmittalDate { get; set; } int TotalPages { get; set; } Delivery DeliveryMethod { get; set; }
string OtherDeliveryMethod { get; set; } string PhaseNumber { get; set; } bool Reimbursable { get; set; } bool Final { get; set; } IList<CopyTo> CopyToList { get; } } }
This interface contains all of the common properties associated with a document Transmittal, including the associated Project.
You may have noticed that there are no Routing Items in this interface, and that is because when I did my analysis of the existing application, I noticed that not all transmittals were always routable. To account for transmittals that could be routed, I created another interface, the IRoutableTransmittal
interface:
using System; using System.Collections.Generic; namespace SmartCA.Model.Transmittals { public interface IRoutableTransmittal : ITransmittal { IList<RoutingItem> RoutingItems { get; } } }
This interface simply adds to the existing ITransmittal
interface and adds a property for the Routing Items. The next step in the refactoring was to modify the Submittal
and RequestForInformation
classes to implement these interfaces. Luckily, these classes already contain all of these properties, so it was a very simple refactoring to change the class signatures:
public class RequestForInformation:
EntityBase,
IRoutableTransmittal public class Submittal:
EntityBase,
IRoutableTransmittal
Remember, in the .NET Framework, you can only inherit from one base class, but you can implement as many interfaces as you like.
The next step was to add a new Repository to the inheritance chain so that the Transmittal-related behavior could be shared by both the SubmittalRepository
and the RequestForInformationRepository
classes. So I made a new Repository and called it SqlCeTransmittalRepository<T>
. This Repository is an abstract class that inherits from SqlCeRepositoryBase<T>
.
Here is the signature for the Repository:
public abstract class SqlCeTransmittalRepository<T> : SqlCeRepositoryBase<T> where T : EntityBase, ITransmittal
The next thing to add to this new class was the pass-through constructors to the SqlCeRepositoryBase<T>
class:
#region Constructors protected SqlCeTransmittalRepository() : this(null) { } protected SqlCeTransmittalRepository(IUnitOfWork unitOfWork) : base(unitOfWork) { } #endregion
Figure 6.5 shows a diagram of what the new RFI Aggregate Repository inheritance chain looks like now.
Figure 6.5 shows a diagram of what the new RFI Aggregate Repository inheritance chain looks like now.
The next function that I needed to perform was splitting out the code that deals with the CopyTo
instances.
This method is now refactored to take a type of EntityBase
and ITransmittal
for its arguments:
protected void AppendCopyToList(T transmittal) { StringBuilder builder = new StringBuilder(100); builder.Append(string.Format("SELECT * FROM {0}CopyList", this.EntityName)); builder.Append(string.Format(" WHERE {0} = '{1}';", this.KeyFieldName, transmittal.Key)); using (IDataReader reader = this.ExecuteReader(builder.ToString())) { while (reader.Read()) { transmittal.CopyToList.Add(TransmittalFactory.BuildCopyTo( transmittal.ProjectKey, reader)); } } }
This code is essentially the same as the code from the old version of the SubmittalRepository
, but instead of being hard-coded to a Submittal
instance or a RequestForInformation
instance, it now only relies upon EntityBase
and ITransmittal
. The queries have been changed to be more generic as well. An important thing to note about this change is that I needed also to refactor the SqlCeRepositoryBase<T>
class a little bit, with the addition of the EntityName
and KeyFieldName
properties. Also, all classes deriving from SqlCeRepositoryBase<T>
now need to implement the following new abstract methods:
protected abstract string GetEntityName(); protected abstract string GetKeyFieldName();
Here is an example of how the RequestForInformationRepository
class is implementing the methods:
#region GetEntityName protected override string GetEntityName() { return "RequestForInformation"; } #endregion #region GetKeyFieldName protected override string GetKeyFieldName()
{ return RequestForInformationFactory.FieldNames.RequestForInformationId; } #endregion
These follow the same Template Method Pattern that the SqlCeRepositoryBase<T>
class has been implementing all along, as two more calls have been added to its constructor:
protected SqlCeRepositoryBase(IUnitOfWork unitOfWork) : base(unitOfWork) { this.database = DatabaseFactory.CreateDatabase(); this.entityFactory = EntityFactoryBuilder.BuildFactory<T>(); this.childCallbacks = new Dictionary<string, AppendChildData>(); this.BuildChildCallbacks(); this.baseQuery = this.GetBaseQuery(); this.baseWhereClause = this.GetBaseWhereClause(); this.entityName = this.GetEntityName(); this.keyFieldName = this.GetKeyFieldName(); }
This method has been refactored very similarly to the AppendCopyToList
method.
protected void DeleteCopyToList(T transmittal) { string query = string.Format("DELETE FROM {0}CopyList {1}", this.EntityName, this.BuildBaseWhereClause(transmittal.Key)); this.Database.ExecuteNonQuery( this.Database.GetSqlStringCommand(query)); }
The only change in this method is that, just like the AppendCopyToList
method, it has been made more generic and relies upon the EntityBase
class as well as the ITransmittal
interface in order to build its SQL query.
These methods keep following the same pattern in their refactoring as the others:
protected void InsertCopyToList(T transmittal) { foreach (CopyTo copyTo in transmittal.CopyToList) { this.InsertCopyTo(copyTo, transmittal.Key); } } private void InsertCopyTo(CopyTo copyTo, object key)
{ StringBuilder builder = new StringBuilder(100); builder.Append(string.Format("INSERT INTO {0}CopyList ({1},{2},{3}) ", this.EntityName, this.KeyFieldName, TransmittalFactory.FieldNames.ProjectContactId, TransmittalFactory.FieldNames.Notes)); builder.Append(string.Format("VALUES ({0},{1},{2});", DataHelper.GetSqlValue(key), DataHelper.GetSqlValue(copyTo.Contact.Key), DataHelper.GetSqlValue(copyTo.Notes))); this.Database.ExecuteNonQuery( this.Database.GetSqlStringCommand(builder.ToString())); }
Like the other methods, these methods are also aided by the new interface and the new properties and abstract methods of the SqlCeRepositoryBase<T>
class.
The next piece of functionality that needed refactoring was the notion of routable Transmittals. As shown earlier, the IRoutableTransmital
interface shows that a routable Transmittal is one that has RoutingItems
associated with it.
public interface IRoutableTransmittal : ITransmittal { IList<RoutingItem> RoutingItems { get; } }
So the next logical step was to add a new abstract Repository to house the necessary code for RoutingItems
. That class is the SqlCeRoutableTransmittalRepository<T>
class, and its signature is shown here:
public abstract class SqlCeRoutableTransmittalRepository<T> : SqlCeTransmittalRepository<T> where T : EntityBase, IRoutableTransmittal
I am not going to show its pass-through constructors, since they are almost identical to the constructors in the SqlCeTransmittalRepository<T>
class. As you might expect, this class is intended to isolate the code that deals with RoutingItem
instances. It has three protected methods in it, AppendRoutingItems, DeleteRoutingItems
, and InsertRoutingItems
, and two private methods, DeleteRoutingItem
and InsertRoutingItem
. I am not going to show the code for these methods, as they are nearly identical to the old methods, and they have simply been modified, like all of the other newly refactored methods, to be more generic. Instead of depending upon the ITransmittal
interface, the new methods in the SqlCeRoutableTransmittalRepository<T>
class depend upon the IRoutableTransmittal
interface.
Following the same steps that I have shown before to implement the Unit of Work pattern, I need to override the following three methods: PersistNewItem(RequestForInformation item), PersistUpdatedItem(RequestForInformation item)
, and PersistDeletedItem(RequestForInformation item)
.
The first method override for the RequestForInformationRepository
's Unit of Work implementation is the PersistNewItem
method:
protected override void PersistNewItem(RequestForInformation item) { StringBuilder builder = new StringBuilder(100); builder.Append(string.Format("INSERT INTO RequestForInformation ({0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17}, {18},{19},{20},{21},{22},{23},{24},{25}) ", RequestForInformationFactory.FieldNames.RequestForInformationId, ProjectFactory.FieldNames.ProjectId, RequestForInformationFactory.FieldNames.RequestForInformationNumber, RequestForInformationFactory.FieldNames.TransmittalDate, RequestForInformationFactory.FieldNames.ProjectContactId, RequestForInformationFactory.FieldNames.TotalPages, RequestForInformationFactory.FieldNames.DeliveryMethod, RequestForInformationFactory.FieldNames.OtherDeliveryMethod, RequestForInformationFactory.FieldNames.PhaseNumber, RequestForInformationFactory.FieldNames.Reimbursable, RequestForInformationFactory.FieldNames.Final, RequestForInformationFactory.FieldNames.DateReceived, RequestForInformationFactory.FieldNames.DateRequestedBy, CompanyFactory.FieldNames.CompanyId, SubmittalFactory.FieldNames.SpecificationSectionId, RequestForInformationFactory.FieldNames.Question, RequestForInformationFactory.FieldNames.Description, RequestForInformationFactory.FieldNames.ContractorProposedSolution, RequestForInformationFactory.FieldNames.NoChange, RequestForInformationFactory.FieldNames.Cause, RequestForInformationFactory.FieldNames.Origin, RequestForInformationFactory.FieldNames.ItemStatusId, RequestForInformationFactory.FieldNames.DateToField, RequestForInformationFactory.FieldNames.ShortAnswer, RequestForInformationFactory.FieldNames.LongAnswer, RequestForInformationFactory.FieldNames.Remarks )); builder.Append(string.Format("VALUES ({0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17}, {18},{19},{20},{21},{22},{23},{24},{25});", DataHelper.GetSqlValue(item.Key), DataHelper.GetSqlValue(item.ProjectKey), DataHelper.GetSqlValue(item.Number), DataHelper.GetSqlValue(item.TransmittalDate), DataHelper.GetSqlValue(item.From.Key), DataHelper.GetSqlValue(item.TotalPages), DataHelper.GetSqlValue(item.DeliveryMethod), DataHelper.GetSqlValue(item.OtherDeliveryMethod), DataHelper.GetSqlValue(item.PhaseNumber), DataHelper.GetSqlValue(item.Reimbursable), DataHelper.GetSqlValue(item.Final), DataHelper.GetSqlValue(item.DateReceived), DataHelper.GetSqlValue(item.DateRequestedBy),
DataHelper.GetSqlValue(item.Contractor), DataHelper.GetSqlValue(item.SpecSection), DataHelper.GetSqlValue(item.Question), DataHelper.GetSqlValue(item.Description), DataHelper.GetSqlValue(item.ContractorProposedSolution), DataHelper.GetSqlValue(item.Change), DataHelper.GetSqlValue(item.Cause), DataHelper.GetSqlValue(item.Origin), DataHelper.GetSqlValue(item.Status.Id), DataHelper.GetSqlValue(item.DateToField), DataHelper.GetSqlValue(item.ShortAnswer), DataHelper.GetSqlValue(item.LongAnswer), DataHelper.GetSqlValue(item.Remarks))); this.Database.ExecuteNonQuery( this.Database.GetSqlStringCommand(builder.ToString())); // Now do the child objects this.InsertCopyToList(item); this.InsertRoutingItems(item); }
The code builds up a large insert statement composed of the values from the RequestForInformation
instance and then executes the query using the Microsoft Enterprise Library's Database
object. After the insert statement has been executed, I have to account for inserting the CopyTo
and RoutingItem
instances for the RFI. I do this by calling the newly refactored InsertCopyToList
and InsertRoutingItems
methods, which all take an IRoutableTransmittal
instance (which the RFI class implements) as their only argument.
PersistUpdatedItem
first does an update to the RequestForInformation
table:
protected override void PersistUpdatedItem(RequestForInformation item) { StringBuilder builder = new StringBuilder(100); builder.Append("UPDATE RequestForInformation SET "); builder.Append(string.Format("{0} = {1}", RequestForInformationFactory.FieldNames .RequestForInformationNumber, DataHelper.GetSqlValue(item.Number))); builder.Append(string.Format(",{0} = {1}", RequestForInformationFactory.FieldNames.TransmittalDate, DataHelper.GetSqlValue(item.TransmittalDate))); ************************************************************************** builder.Append(string.Format(",{0} = {1}", RequestForInformationFactory.FieldNames.Remarks, DataHelper.GetSqlValue(item.Remarks))); builder.Append(" ");
builder.Append(this.BuildBaseWhereClause(item.Key)); this.Database.ExecuteNonQuery( this.Database.GetSqlStringCommand(builder.ToString())); // Now do the child objects // First, delete the existing ones this.DeleteCopyToList(item); this.DeleteRoutingItems(item); // Now, add the current ones this.InsertCopyToList(item); this.InsertRoutingItems(item); }
I have omitted several lines of repetitive code building the SQL update statement in the middle of the code in order save you from the boring code. The removed lines are represented by a single line of asterisks.
The second part of the method then uses the newly refactored DeleteCopyToList
and DeleteRoutingItems
helper methods to delete all of the child objects of the RFI, and then uses the also newly refactored InsertCopyToList
and InsertRoutingItems
helper methods to add the existing child objects from the RFI to the database.
As I was writing the last method in RequestForInformationRepository
to override, PersistDeletedItem
, I realized that I could refactor that back into the base classes as well. Originally, the code I wrote looked like this:
protected override void PersistDeletedItem(RequestForInformation item) { // Delete the child objects first this.DeleteCopyToList(item); this.DeleteRoutingItems(item); // Now delete the RFI string query = string.Format("DELETE FROM RequestForInformation {0}", this.BuildBaseWhereClause(item.Key)); this.Database.ExecuteNonQuery( this.Database.GetSqlStringCommand(query)); }
After analyzing the code, I saw another opportunity to refactor it back into the SqlCeRepositoryBase<T>
and SqlCeRoutableTransmittalRepository<T>
classes. I will start with the SqlCeRepositoryBase<T>
implementation:
protected override void PersistDeletedItem(T item) { // Delete the Entity string query = string.Format("DELETE FROM {0} {1}",
this.entityName, this.BuildBaseWhereClause(item.Key)); this.Database.ExecuteNonQuery( this.Database.GetSqlStringCommand(query)); }
This was made possible by the Template Method Pattern implemented in the earlier refactoring for the EntityName
and EntityKey
properties of the SqlCeRepositoryBase<T>
class. This method is now generic enough to delete any Entity from the database. So the logical question now is, "what about when the Entity has children that must be deleted first?" The answer comes in overriding the PersistDeletedItem
method in derived classes. I took this a step further with the concept of deleting Transmittals and added functionality in the SqlCeRoutableTransmittalRepository<T>
class to do just this:
protected override void PersistDeletedItem(T transmittal) { // Delete the child objects first this.DeleteCopyToList(transmittal); this.DeleteRoutingItems(transmittal); // Delete the transmittal entity base.PersistDeletedItem(transmittal); }
This is great because now I can delete whatever child objects I want to delete first, and then call the base class, in this case SqlCeRepositoryBase<T>
, to do the rest. So in the SubmittalRepository
, the implementation now becomes:
protected override void PersistDeletedItem(Submittal item) { // Delete the child objects first this.DeleteTrackingItems(item); // Now delete the submittal and its associated // transmittal objects base.PersistDeletedItem(item); }
Because Tracking Items are not part of the ITransmittal
interface, I needed to delete these first, and then by calling the base class, in this case the SqlCeRoutableTransmittalRepository<T>
class, I am able to delete the rest of the child objects (the CopyToList
and the RoutingItems
) as well as the Entity itself. What is even better is that in the RequestForInformationRepository
class (and in a few other repositories), the need to override the PersistDeletedItem
method goes away completely!
Still in this application, the only Service
classes I have implemented up to this point are all Service
classes that live in the domain model layer and are acting as facades to their respective Repository interfaces.
The RequestForInformationService
class is responsible for retrieving and saving RequestForInformation
instances.
using System; using SmartCA.Infrastructure; using SmartCA.Infrastructure.RepositoryFramework; using SmartCA.Model.Projects; using System.Collections.Generic; namespace SmartCA.Model.RFI { public static class RequestForInformationService { private static IRequestForInformationRepository repository; private static IUnitOfWork unitOfWork; static RequestForInformationService() { RequestForInformationService.unitOfWork = new UnitOfWork(); RequestForInformationService.repository = RepositoryFactory.GetRepository<IRequestForInformationRepository, RequestForInformation>(RequestForInformationService.unitOfWork); } public static IList<RequestForInformation> GetRequestsForInformation(Project project) { return RequestForInformationService.repository.FindBy(project); } public static void SaveRequestForInformation(RequestForInformation rfi) { RequestForInformationService.repository[rfi.Key] = rfi; RequestForInformationService.unitOfWork.Commit(); } } }
This class is mainly just acting as a façade in front of the IRequestForInformationRepository
instance. There is nothing really new in this Service
class compared to the other ones.
Following the same patterns for all ViewModel
classes as before, the RequestForInformationViewModel
class adapts the RFI Aggregate from the domain model to the UI. When I started coding the ViewModel for the RFI I noticed that there was a lot in common between the SubmittalViewModel
and the RequestForInformationViewModel
, so I did another major refactoring and created a new abstract ViewModel
class called, you guessed it, the TransmittalViewModel<T>
class.
This class is very similar to the SqlCeTransmittalRepository<T>
and SqlCeRoutableTransmittalRepository<T>
classes that I showed earlier. It is the same concept again, which is to refactor common functionality into an abstract base class and have future classes that share the same functionality inherit them from the new base class. In this case, just like with the Repository
classes and the SqlCeRepositoryBase<T>
class, there is already an abstract base class that my ViewModel
classes inherit from, and that is the ViewModel
class. The TransmittalViewModel<T>
class will be extending this class and it will be abstract as well. Here is the signature for the class:
public abstract class TransmittalViewModel<T> : ViewModel where T : EntityBase, IRoutableTransmittal
This is following the same pattern as the SqlCeTransmittalRepository<T>
and SqlCeRoutableTransmittalRepository<T>
classes, since it is a generic class and is using constraints to make sure that the generic class is an EntityBase
that implements the IRoutableTransmittal
interface.
My goal with this class was to lift all of the Transmittal behavior out of the SubmittalViewModel
class and put it into this class. Therefore, the constructor code you see below should look very much like the old SubmittalViewModel
code, with all references to anything named "submittal" changed to "transmittal."
#region Constructors public TransmittalViewModel() : this(null) { } public TransmittalViewModel(IView view) : base(view) { this.currentTransmittal = null; this.transmittalList = this.GetTransmittals(); this.transmittals = new CollectionView(this.transmittalList); this.specificationSections = SubmittalService.GetSpecificationSections(); this.itemStatuses = SubmittalService.GetItemStatuses(); this.mutableCopyToList = new BindingList<MutableCopyTo>(); this.routingItems = new BindingList<RoutingItem>(); this.deliveryMethods = new CollectionView( Enum.GetNames(typeof(Delivery))); this.disciplines = SubmittalService.GetDisciplines(); this.saveCommand = new DelegateCommand(this.SaveCommandHandler); this.newCommand = new DelegateCommand(this.NewCommandHandler); this.deleteCopyToCommand = new DelegateCommand(this.DeleteCopyToCommandHandler); this.deleteRoutingItemCommand = new DelegateCommand(this.DeleteRoutingItemCommandHandler); } #endregion
I was able to reuse almost everything in the old constructor, except for the Tracking Items, which are not part of what I have defined for a Transmittal. I decided for now to leave the calls in to the SubmittalService
, although that class is also a candidate for refactoring.
Notice how the GetTransmittals
method is called in order to initialize the list of Transmittals. This is an abstract method of the TransmittalViewModel<T>
class, and thus I am once again using the Template Method pattern. I will show more on this method later.
The CurrentTransmittal
property is extremely similar to the old CurrentSubmittal
property of the SubmitalViewModel
class:
public T CurrentTransmittal { get { return this.currentTransmittal; } set { if (this.currentTransmittal != value) { this.currentTransmittal = value; this.OnPropertyChanged(Constants .CurrentTransmittalPropertyName); this.OnPropertyChanged("Status"); this.saveCommand.IsEnabled = (this.currentTransmittal != null); this.PopulateTransmittalChildren(); } } }
The only difference between this code and the SubmitalViewModel CurrentTransmittal
property code is that this code is more generic. Don't you just love Generics? I bet you can't tell that I do!
The PopulateSubmittalChildren
method has been changed to the PopulateSubmittalChildren
method. Here is the old method:
private void PopulateSubmittalChildren() { this.PopulateMutableCopyToList(); this.PopulateRoutingItems(); this.PopulateTrackingItems(); }
And here is the new method:
protected virtual void PopulateTransmittalChildren() { this.PopulateMutableCopyToList(); this.PopulateRoutingItems(); }
The only difference in this method is that it no longer tries to populate the Tracking Items data, and that is exactly why I made it virtual, because derived classes may need to override this method in order to populate their own child objects as necessary.
The PopulateMutableCopyToList
method has been changed from this:
private void PopulateMutableCopyToList() { if (this.currentSubmittal != null) { this.mutableCopyToList.Clear(); foreach (CopyTo copyTo in this.currentSubmittal.CopyToList) { this.mutableCopyToList.Add(new MutableCopyTo(copyTo)); } this.OnPropertyChanged(Constants.MutableCopyToListPropertyName); } }
To this:
private void PopulateMutableCopyToList() { if (this.currentTransmittal != null) { this.mutableCopyToList.Clear(); foreach (CopyTo copyTo in this.currentTransmittal.CopyToList) { this.mutableCopyToList.Add(new MutableCopyTo(copyTo)); } this.OnPropertyChanged(Constants.MutableCopyToListPropertyName); } }
I am not going to show the PopulateRoutingItems
method because it follows the exact same pattern as the PopulateMutableCopyToList
method.
Refactoring the Command Handler methods was a little bit trickier than some of the other methods in the TransmittalViewModel
class. The SaveCommandHandler
and the NewCommandHandler
methods both had to be marked as virtual, and that is because I could only pull so much out of them into this, and the rest that is specific to the derived class must be overridden.
For example, the NewCommandHandler
went from this:
private void NewCommandHandler(object sender, EventArgs e) { Submittal newSubmittal = new Submittal( this.currentSubmittal.SpecSection, this.currentSubmittal.ProjectKey);
newSubmittal.SpecSectionSecondaryIndex = "01"; this.currentSubmittal = null; this.mutableCopyToList.Clear(); this.routingItems.Clear(); this.trackingItems.Clear(); this.CurrentObjectState = ObjectState.New; this.OnPropertyChanged( Constants.CurrentSubmittalPropertyName); this.submittalsList.Add(newSubmittal); this.submittals.Refresh(); this.submittals.MoveCurrentToLast(); }
protected virtual void NewCommandHandler(object sender, EventArgs e) { this.currentTransmittal = null; this.mutableCopyToList.Clear(); this.routingItems.Clear(); this.CurrentObjectState = ObjectState.New; this.OnPropertyChanged( Constants.CurrentTransmittalPropertyName); }
The code that initializes the new Entity (i.e., the Submittal
class in the first example above) had to be removed and must be overridden in the derived ViewModel
. I also needed to remove the code that added the Submittal to the list of Submittals because I needed that to happen last in the derived class, after this code executes.
As seen in the constructor, the GetTransmittals
abstract method is called in order to initialize the list of Transmittals for the class. Here is the signature of this method:
protected abstract List<T> GetTransmittals();
This is great because by doing this I am delegating the derived class to get the right list of objects, yet I can still code against that list in my base class. Combining the Template Method pattern with Generics is a great thing!
Now the fruits of our ViewModel refactoring labor start to pay off. The code inside of the RequestForInformationViewModel
and SubmittalViewModel
classes has been reduced significantly. Here is what the signature of the RequestForInformationViewModel
class looks like when deriving from the TransmittalViewModel<T>
class:
public class RequestForInformationViewModel : TransmittalViewModel<RequestForInformation>
Notice how the generic parameter from the TransmittalViewModel<T>
class is replaced by the RequestForInformation
class.
The constructors for the RequestForInformationViewModel
class now are mostly pass-through. Here is the old SubmittalViewModel
constructor:
#region Constructors public SubmittalViewModel() : this(null) { } public SubmittalViewModel(IView view) : base(view) { this.currentSubmittal = null; this.submittalsList = new List<Submittal>( SubmittalService.GetSubmittals( UserSession.CurrentProject)); this.submittals = new CollectionView(this.submittalsList); this.specificationSections = SubmittalService.GetSpecificationSections(); this.submittalStatuses = SubmittalService.GetSubmittalStatuses(); this.toList = UserSession.CurrentProject.Contacts; this.mutableCopyToList = new BindingList<MutableCopyTo>(); this.routingItems = new BindingList<RoutingItem>(); this.trackingItems = new BindingList<TrackingItem>(); this.fromList = EmployeeService.GetEmployees(); this.trackingStatusValues = new CollectionView( Enum.GetNames(typeof(ActionStatus))); this.deliveryMethods = new CollectionView( Enum.GetNames(typeof(Delivery))); this.disciplines = SubmittalService.GetDisciplines(); this.saveCommand = new DelegateCommand(this.SaveCommandHandler); this.newCommand = new DelegateCommand(this.NewCommandHandler); this.deleteCopyToCommand = new DelegateCommand(this.DeleteCopyToCommandHandler); this.deleteRoutingItemCommand = new DelegateCommand(this.DeleteRoutingItemCommandHandler); this.deleteTrackingItemCommand = new DelegateCommand(this.DeleteTrackingItemCommandHandler); } #endregion
Here are the new constructors for the RequestForInformationViewModel
class:
#region Constructors public RequestForInformationViewModel() : this(null) { } public RequestForInformationViewModel(IView view) : base(view) { this.toList = UserSession.CurrentProject.Contacts; this.fromList = UserSession.CurrentProject.Contacts; } #endregion
That's quite a reduction in code! The toList
and fromList
private fields are not contained in the base class and therefore need to be initialized here.
There are not many properties left to implement in the RequestForInformationViewModel
class. Here is all of the code for the properties:
#region Properties public IList<ProjectContact> ToList { get { return this.toList; } } public IList<ProjectContact> FromList { get { return this.fromList; } } #endregion
The only command handler methods that I need to override in the RequestForInformationViewModel
class are the NewCommandHandler
and SaveCommandHandler
methods. The DeleteCommandHandler
method is completely taken care of by the base class.
Because the NewCommandHandler
method in the base class was marked as virtual, I am still able to use it as well as add my own functionality:
protected override void NewCommandHandler(object sender, EventArgs e) { base.NewCommandHandler(sender, e); RequestForInformation newRfi = new RequestForInformation( this.CurrentTransmittal.ProjectKey, this.CurrentTransmittal.Number + 1); this.TransmittalList.Add(newRfi); this.Transmittals.Refresh(); this.Transmittals.MoveCurrentToLast(); }
Notice how on the first line of the method I call the same method in the base class. This allows me to reuse the common code yet gives me the flexibility to do my own housekeeping when creating the new RequestForInformation
instance.
As I mentioned before, the GetTransmittals
method is overridden in the derived classes because only they know where to get their data; the base class does not need to know about that:
#region GetTransmittals protected override List<RequestForInformation> GetTransmittals() { return new List<RequestForInformation>(RequestForInformationService.GetRequestsForInformation( UserSession.CurrentProject)); } #endregion
In this case of the RequestForInformationViewModel
class's override, I am simply calling out to the RequestForInformationService
to get the list of RequestForInformation
instances.
The View for RFIs is almost exactly identical to that for Submittals. Figure 6.6 shows what the form looks like at run time.
Following the same pattern as before, the form is split into two parts: the one on the left is for selecting an RFI to edit, and the one on the right is for editing the selected RFI. The New button adds a new RFI to the list. The Save and Cancel buttons both deal with the currently selected RFI.
I really do not need to show any of the XAML code for this form because there is really not much in it that is different from the Submittal form.
In this chapter, I introduced the concept of a Request for Information (RFI) in the construction industry and then used that concept to model the RFI Aggregate. I also introduced a new concept into the domain, called the Specification Pattern. This made some of the business rule modeling very clear by bringing business rules out from underneath class methods and placing them into their own Specification
classes. I then defined the boundaries for the RFI Aggregate, as well as implementing all of the necessary domain model and Infrastructure
classes necessary to work with those classes. I also did some major refactoring in this Chapter for the repositories and ViewModels dealing with the new concept of Transmittals.
3.21.34.0