Introduction
Break down a larger problem into smaller parts and solve each of the smaller parts - a central pillar in many engineering tasks. This also applies to software engineering - divide and conquer. This is a simple working example of how this can be done for a web application.
You can use this technique in almost any project where you have a team of programmers working on a UI tied to back-end code. This example just happens to use MVC4 and ADO.NET. This strategy is agnostic of the persistence implementations or programming language or any project specific technologies.
The usefulness of this is two fold. If you can figure out how to break down a problem into smaller components, the tasks then become more manageable and easier to complete. Also, the work can now be divided between programmers.
A natural separation point in software development has always been UI and "other" code. This was not always possible, for example, in case of ASP pages, you often see database, business logic, and user interface elements side by side in the same file. Now with more advanced .NET Frameworks and better design templates like MVC, we can more cleanly separate these elements.
This project shows two methods of mocking up data quickly so that the UI developer can go off and start working on the UI and the back-end developer has enough information to go off and start working up the business logic layer, database layer, and database. The meeting point will be the controllers where we will get to integrate the two parts.
This example is useful because on most projects, there will be several developers involved and finding natural ways of breaking up tasks and jump starting a project into actual coding can be useful. It gets you down the road to working production code.
Background
Create a new MVC4 basic project .NET 4.5 framework. Make sure you use Razor as your view engine and no unit tests. Name it anything you'd like. I named my solution and project the same - Rates
. This solution will contain just the one project.
You need to be comfortable with the MVC project type in .NET and need to have worked through a project or two before attempting to do this project. A lot of the setup for this project assumes you have a MVC background and this tip will not explain the details of each of these steps.
Using the Code
We are starting from a basic project because it allows us to jump right into the interesting coding parts without having to worry about setting up some of the basic libraries and folders/files used in MVC4. I find it easier to just delete out the things I don't need from the basic project versus getting things to work nicely from an empty project. I think there is value in understanding where things are coming from when you select a project type, but that's for another article.
Now, create these additional folders that could be used later - Home under the Views folder, DAL (database access layer), BLL (business logic layer), and the Types folders under the root. We are just setting some things up. Some of these will not get touched. I will leave the other layers to you if you want to use a database. In the end, you should have a folder structure that looks like this. Organization and naming is vital as a project gets bigger and more complicated, so get into the habit.
Under the Views\Home folder, right click and add a view called "Index
". Take the default options. You should now have an Index.cshtml page in the Home folder. Now go the Controllers folder and right click and click Add -> Controller... and rename the controller "HomeController
". You should now have a HomeController.cs file in the Controllers folder. Controller names and relative locations of the files do matter because of routing in the MVC framework so be exact on your naming and folder structure.
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
You will get the above piece of code free when you create the controller. Run the project and you should be able to see the Index page in the browser at URL http://localhost:53834/Home/Index or http://localhost:53834. Your port number may be different than mine (53834). Now everything is tested and working.
Now we are ready to create a useful view. Create a new view under the Views\Home folder called "RateConfiguration
". Add two new classes into the Models folder. The first class will be the RateModel
class which represents the main object of the table inside the RateConfiguration.cshtml page.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.IO;
using System.Xml;
namespace Rates.Models
{
public class RateModel
{
public RateModel()
{
}
public String ROW_ID
{
get;
set;
}
public Int32 SCENARIO_ID
{
get;
set;
}
public Int32? PK_SCENARIO_ID
{
get;
set;
}
public Int32 STORE_NUMBER
{
get;
set;
}
public Int32 ZONE
{
get;
set;
}
public Int32 SECTION
{
get;
set;
}
public Int32? DEPARTMENT
{
get;
set;
}
public Boolean DEPARTMENT_OVERRIDE
{
get;
set;
}
public Decimal PRIMARY_RATE
{
get;
set;
}
public Decimal SECONDARY_RATE
{
get;
set;
}
public Decimal? DEPARTMENT_RATE
{
get;
set;
}
public String CONFIGURATION_TYPE
{
get;
set;
}
}
}
The next class we need is the RateCollectionModel
class which is a collection for RateModels
. We will use this to iterate through to build the HTML table in our main view. This class is where the main C# work is being done. If you look at the two constructors, you'll see that we set the RateCollection
property. We will pass the RateCollectionModel
into our view and use it to build out our HTML table.
Our data source for that property right now is one of two possible options. You will see that in the methods of the RateCollectionModel
class, either an XML file is used or a run-time set of RateModel
objects. Eventually, you could make the data source any database. For now, we are using these method calls to build our RateCollectionModel
object so we can build our view. Once we get the RateCollectionModel
methods working and we can call them from our home controller, we can split out the main tasks - UI development work and the back-end code development.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.Xml;
using Rates.Types;
namespace Rates.Models
{
public class RateCollectionModel
{
public IEnumerable<ratemodel> RateCollection { get; set; }
private DataSet RateDataSet { get; set; }
public RateCollectionModel()
{
this.RateDataSet = this.GetTestDataByObjects();
this.RateCollection = this.DataSetToRateCollection(this.RateDataSet);
}
public RateCollectionModel(RateOptions iGetBy)
{
switch (iGetBy)
{
case RateOptions.xml :
this.RateDataSet = this.GetTestDataSetByXML();
this.RateCollection = this.DataSetToRateCollection(this.RateDataSet);
break;
case RateOptions.objects :
this.RateDataSet = this.GetTestDataByObjects();
this.RateCollection = this.DataSetToRateCollection(this.RateDataSet);
break;
case RateOptions.extensionmethod :
List<ratemodel> lList = new List<ratemodel>()
{
new RateModel() { SCENARIO_ID = 1, CONFIGURATION_TYPE = "SCENARIO",
STORE_NUMBER = 6010, ZONE = 10, SECTION = 324, PK_SCENARIO_ID = 1,
PRIMARY_RATE = 4.0M, SECONDARY_RATE = 4.5M, DEPARTMENT = 1000,
DEPARTMENT_RATE = 9.0M, DEPARTMENT_OVERRIDE = true },
new RateModel() { SCENARIO_ID = 1, CONFIGURATION_TYPE = "SCENARIO",
STORE_NUMBER = 6010, ZONE = 20, SECTION = 230, PK_SCENARIO_ID = 2,
PRIMARY_RATE = 4.0M, SECONDARY_RATE = 4.5M, DEPARTMENT = 2000,
DEPARTMENT_RATE = 8.5M, DEPARTMENT_OVERRIDE = true },
new RateModel() { SCENARIO_ID = 1, CONFIGURATION_TYPE = "PRODUCTION",
STORE_NUMBER = 6010, ZONE = 20, SECTION = 220, PK_SCENARIO_ID = null,
PRIMARY_RATE = 4.0M, SECONDARY_RATE = 4.5M, DEPARTMENT = null,
DEPARTMENT_RATE = null, DEPARTMENT_OVERRIDE = false }
};
this.RateCollection = this.DataSetToRateCollection(lList.ToDataSet<ratemodel>("Rate"));
break;
default:
this.RateCollection = null;
break;
}
}
private List<ratemodel> DataSetToRateCollection(DataSet ds)
{
DataTable dt = ds.Tables["Rate"];
RateModel lRateModel = null;
List<ratemodel> lListRateModel = new List<ratemodel>();
foreach (DataRow row in dt.Rows)
{
lRateModel = new RateModel();
if (String.IsNullOrEmpty(row["PK_SCENARIO_ID"].ToString()))
{
lRateModel.PK_SCENARIO_ID = null;
}
else
{
lRateModel.PK_SCENARIO_ID = Convert.ToInt32(row["PK_SCENARIO_ID"]);
}
lRateModel.STORE_NUMBER = Convert.ToInt32(row["STORE_NUMBER"]);
lRateModel.ZONE = Convert.ToInt32(row["ZONE"]);
lRateModel.SECTION = Convert.ToInt32(row["SECTION"]);
if (String.IsNullOrEmpty(row["DEPARTMENT"].ToString()))
{
lRateModel.DEPARTMENT = null;
}
else
{
lRateModel.DEPARTMENT = Convert.ToInt32(row["DEPARTMENT"]);
}
lRateModel.PRIMARY_RATE = Convert.ToDecimal(row["PRIMARY_RATE"]);
lRateModel.SECONDARY_RATE = Convert.ToDecimal(row["SECONDARY_RATE"]);
if (String.IsNullOrEmpty(row["DEPARTMENT_RATE"].ToString()))
{
lRateModel.DEPARTMENT_RATE = null;
}
else {
lRateModel.DEPARTMENT_RATE = Convert.ToDecimal(row["DEPARTMENT_RATE"]);
}
if (String.IsNullOrEmpty(row["DEPARTMENT_OVERRIDE"].ToString()))
{
lRateModel.DEPARTMENT_OVERRIDE = false;
}
else
{
lRateModel.DEPARTMENT_OVERRIDE = Convert.ToBoolean(row["DEPARTMENT_OVERRIDE"]);
}
lRateModel.CONFIGURATION_TYPE = row["CONFIGURATION_TYPE"].ToString();
lRateModel.SCENARIO_ID = Convert.ToInt32(row["SCENARIO_ID"]);
lListRateModel.Add(lRateModel);
}
return lListRateModel;
}
private DataSet GetTestDataByObjects() {
List<ratemodel> collectionOfRateModel = new List<ratemodel>()
{
new RateModel() { SCENARIO_ID = 1, CONFIGURATION_TYPE = "SCENARIO",
STORE_NUMBER = 6010, ZONE = 10, SECTION = 324, PK_SCENARIO_ID = 1,
PRIMARY_RATE = 4.0M, SECONDARY_RATE = 4.5M, DEPARTMENT = 1000,
DEPARTMENT_RATE = 9.0M, DEPARTMENT_OVERRIDE = true },
new RateModel() { SCENARIO_ID = 1, CONFIGURATION_TYPE = "SCENARIO",
STORE_NUMBER = 6010, ZONE = 20, SECTION = 230, PK_SCENARIO_ID = 2,
PRIMARY_RATE = 4.0M, SECONDARY_RATE = 4.5M, DEPARTMENT = 2000,
DEPARTMENT_RATE = 8.5M, DEPARTMENT_OVERRIDE = true },
new RateModel() { SCENARIO_ID = 1, CONFIGURATION_TYPE = "SCENARIO",
STORE_NUMBER = 6010, ZONE = 10, SECTION = 200, PK_SCENARIO_ID = 1,
PRIMARY_RATE = 4.0M, SECONDARY_RATE = 4.5M, DEPARTMENT = null,
DEPARTMENT_RATE = null, DEPARTMENT_OVERRIDE = false },
new RateModel() { SCENARIO_ID = 1, CONFIGURATION_TYPE = "PRODUCTION",
STORE_NUMBER = 6010, ZONE = 20, SECTION = 210, PK_SCENARIO_ID = null,
PRIMARY_RATE = 4.0M, SECONDARY_RATE = 4.5M, DEPARTMENT = null,
DEPARTMENT_RATE = null, DEPARTMENT_OVERRIDE = false },
new RateModel() { SCENARIO_ID = 1, CONFIGURATION_TYPE = "PRODUCTION",
STORE_NUMBER = 6010, ZONE = 20, SECTION = 220, PK_SCENARIO_ID = null,
PRIMARY_RATE = 4.0M, SECONDARY_RATE = 4.5M, DEPARTMENT = null,
DEPARTMENT_RATE = null, DEPARTMENT_OVERRIDE = false }
};
Type elementType = typeof(RateModel);
DataSet ds = new DataSet();
DataTable t = new DataTable();
t.TableName = "Rate";
ds.Tables.Add(t);
foreach (var propInfo in elementType.GetProperties())
{
Type ColType = Nullable.GetUnderlyingType(propInfo.PropertyType) ??
propInfo.PropertyType;
t.Columns.Add(propInfo.Name, ColType);
}
foreach (RateModel rm in collectionOfRateModel)
{
DataRow dr = t.NewRow();
foreach (var propInfo in elementType.GetProperties()) {
dr[propInfo.Name] = propInfo.GetValue(rm, null) ?? DBNull.Value;
}
t.Rows.Add(dr);
}
return ds;
}
private DataSet GetTestDataSetByXML()
{
XmlReader xmlFile;
xmlFile = XmlReader.Create("C:\\Users\\giancarlo.rhodes\\Documents\\Visual Studio 2013\\
Projects\\Rates\\Rates\\App_Data\\rates.xml", new XmlReaderSettings());
DataSet ds = new DataSet();
ds.ReadXml(xmlFile);
return ds;
}
}
}
Here's an example of creating a RateCollectionModel
object and using one of the constructors to set the RateCollection
property inside a controller.
public ActionResult RateConfiguration() {
RateCollectionModel rcm = new RateCollectionModel(RateOptions.xml);
return View("RateConfiguration", rcm);
}
You can see that in the code above, we are calling the constructor that takes a RateOptions enum
as an argument and that we are in this case using an XML file to set the RateCollection
. Other options we could have used for RateOptions
are "objects
" and "extensionmethod
". These two are very similar in that they are creating objects at run-time and that their data is embedded in the methods and not as separate data. This is fine for our purposes of mocking data and getting started on the UI as soon as possible. You can change the code in the RateConfiguration
method in the home controller to test and walk through the code for the three RateOptions
to see the details how each of these is working. You can also use the default constructor which is commented out above.
The whole point is to get some data mocked up as quickly as possible so we can move on to developing the UI.
Below is the pseudo-code for the RateConfiguration.cshtml. You can look at the actual Rates
project zip file to see the details. We pass a RateCollectionModel
in and then use it inside a foreach
loop to create each row of the HTML table. The header row only needs to be created once so it is part of the if
statement right before we hit the foreach
loop.
@model Rates.Models.RateCollectionModel
@using Rates.Models;
... all the javascript functions
@using (Html.BeginForm("RateConfiguration",
"Home", FormMethod.Post, new { id = "formRate" }))
{
@if (Model.RateCollection.Count() > 0)
{
..... BUILD THE TABLE HEADER
@foreach (RateModel lRate in Model.RateCollection)
{
..... FILL THE TABLE
}
}
}
The cshtml and JavaScript is fairly straightforward. The validation checks are done on the onblur
events within the textbox
es and the one checkbox
. The updating, canceling, and cloning functions are done on the onclick
events for the buttons. The actual database insert
s and update
s will be done as Ajax calls and the plumbing for that is already in place here. You will see the Ajax calls in the JavaScript and you will see the method SaveRowChanges
in the home controller that will be posted to through the Ajax call.
Conclusion
I hope you find this method of mocking data helpful in jump starting your UI development.
History
- 2015-01-08 First draft
- 2015-01-16 Fixed broken images
- 2015-01-29 Changed title and clarified what the example is attempting to show in the first couple paragraphs