Tuesday 25 May 2010

MVC 2, Remote SQL and OS OpenDate Part 1

Using MVC2, remote SQL Connection and OS OpenData Map

I have only had the opportunity to use MVC a couple of times and there are some very good tutorials available. What I wanted to do here was to bring a few of these samples together in a complete micro site.

References:
Nerd Diner
Scottgu
bytes

Tools Used:

Visual Studio 2008 with SP1
ASP.NET MVC 2 (using the Web Platform Installer)
SQL 2005 Express Edition

Services Used:
OS Opendata (Map service)

Mini Brief:

Develop a microsite to allow self administration of the locations of BioDiesel sites. This should use the Ordnancesurvey OS Opendata Map API to display the sites, but also to act as a Geocoder for Post Code lookup.

Only registered users are to be allowed to add, edit or delete sites. Guests can search and view sites.


Process Flow

1. Set up database
1.1. Set up Data Tables

2. Create Model

3. Create Controls
3.1. Index
3.2. Details
3.3. Edit
3.4. Create

4. Create Views
4.1. Index
4.2. Details
5. Create Partial Views
5.1. Edit
5.2. Create

Part 1 - Setting up the database

After creating a new ASP.NET MVC 2 Website we need to alter the web.config to select the correct database to use. By default the MVC framework tries to use a local SQL Express. We are using a remote SQL Express server, which mimics a real business requirement.

To ensure that the MVC Login works we are initially going to set up the database using the aspnet_regsql.exe utility, setup details can be found in many places on the web.

Original Setting





Part 1.1 - Setting up the Data Tables

We need to add two tables to our database, the first one is to hold the location of the BioFuel station.

CREATE TABLE [dbo].[BioStation](
[StationID] [int] IDENTITY(1,1) NOT NULL,
[StationName] [nvarchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
[DateAdded] [datetime] NOT NULL,
[Description] [nvarchar](256) COLLATE Latin1_General_CI_AS NOT NULL,
[Street] [nvarchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
[District] [nvarchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
[Town] [nvarchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
[County] [nvarchar](50) COLLATE Latin1_General_CI_AS NOT NULL,
[Postcode] [nchar](10) COLLATE Latin1_General_CI_AS NOT NULL,
[latitude] [float] NOT NULL,
[longitude] [float] NOT NULL,
CONSTRAINT [PK_BioStation] PRIMARY KEY CLUSTERED
(
[StationID] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

Table two is to hold the details of each stations Bio Fuel prices

CREATE TABLE [dbo].[BioPrice](
[FuelType] [nchar](10) COLLATE Latin1_General_CI_AS NOT NULL,
[StationID] [int] NOT NULL,
[PriceDate] [datetime] NOT NULL,
[PricePerLitre] [decimal](5, 2) NOT NULL,
CONSTRAINT [PK_BioPrice] PRIMARY KEY CLUSTERED
(
[FuelType] ASC,
[StationID] ASC,
[PriceDate] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

The table needs to be joined to maintain data integrity

ALTER TABLE [dbo].[BioPrice] WITH CHECK ADD CONSTRAINT [FK_BioPrice_BioStation] FOREIGN KEY([StationID])
REFERENCES [dbo].[BioStation] ([StationID])
GO
ALTER TABLE [dbo].[BioPrice] CHECK CONSTRAINT [FK_BioPrice_BioStation]

We need to add some default test data, this cam be done using the "Show Table Data" in Visual studio, or as I prefer, using the Management studio. This is easy until we look at the Longitude and Latitude. It would be really usefull if there was only on LongLat system, and in a real system there would be a requirement to break out the location information into a separate table with a primary key of LongLat Type. I am going to be using the OS Opendata so I am going to save the LongLat information as OSGB type.

Part 2 - Creating the Model

Initially I want to use LINQ to SQL, more to give me a better understanding of LINQ but also with the BioPrices table able to hold historic price details, setting up LINQ should be more like a real situation. We need to start by adding the correct class to our Model.



Once the class has been installed we can then add the data tables directly.


Once we have the LINQ to SQL class we need to make a new class to help do some of the work. Create a new BioStationRepository.Class object

using System;
using System.Collections.Generic;
using System.Linq;

namespace BioFuels.Models
{
public class BioStationRepository
{
private BioStationsDataContext db = new BioStationsDataContext();

//Query Method
#region queries
public IQueryable FindAllStations()
{
return db.BioStations;
}

public IQueryable FindStationsByCounty(string County)
{
return from bioStation in db.BioStations
where bioStation.County == County
select bioStation;
}

public BioStation GetStation(int id)
{
return db.BioStations.SingleOrDefault(s => s.StationID == id);
}
#endregion

#region Add methods
public void AddBioStation(BioStation station)
{
db.BioStations.InsertOnSubmit(station);
}

public void AddBioStationPrice(BioPrice price)
{
db.BioPrices.InsertOnSubmit(price);
}

#endregion

public void save()
{
db.SubmitChanges();
}
}
}

With the Index and Details stub, right mouse click and select Add View. This will create a Folder called BioDiesel and the aspx pages required for the views.

Integrating validation is key to ensuring a fully compliant and smooth running web site. Part of this is to do with prevention of SQL Inject attacks. LINQ to SQL handles escaping characters in SQL so helps to prevent this form of attack.

We need to create validation classes for the BioStation and BioPrices classes. Starting with the BioStation Class, create a new partial class (partial so the new class will be absorbed by the class created earlier).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Linq;

namespace BioFuels.Models
{
public partial class BioStation
{
public bool IsValid
{
get
{
return (GetRuleViolations().Count() == 0);
}
}

public IEnumerable GetRuleViolations()
{
if (string.IsNullOrEmpty(StationName))
yield return new RuleViolation("Station Name required", "StationName");

if (string.IsNullOrEmpty(Description))
yield return new RuleViolation("Description required", "Description");

if (string.IsNullOrEmpty(Street))
yield return new RuleViolation("Street Address required", "Street");

if (string.IsNullOrEmpty(District))
yield return new RuleViolation("District required", "District");

if (string.IsNullOrEmpty(Town))
yield return new RuleViolation("Town required", "Town");

if (string.IsNullOrEmpty(County))
yield return new RuleViolation("County required", "County");

if (string.IsNullOrEmpty(Postcode))
yield return new RuleViolation("Postcode required", "Postcode");

yield break;
}

partial void OnValidate(ChangeAction action)
{
if (!IsValid)
throw new ApplicationException("Rule violations prevent saving");
}

}

public partial class BioPrice
{
public bool IsValid
{
get
{
return (GetRuleViolations().Count() == 0);
}
}

public IEnumerable GetRuleViolations()
{
if (string.IsNullOrEmpty(FuelType))
yield return new RuleViolation("FuelType required", "FuelType");

if (PricePerLitre <= 0)
yield return new RuleViolation("Price required", "PricePerLitre");

yield break;
}

partial void OnValidate(ChangeAction action)
{
if (!IsValid)
throw new ApplicationException("Rule violations prevent saving");
}

}

public class RuleViolation
{
public string ErrorMessage { get; set; }
public string PropertyName { get; set; }

public RuleViolation(string errorMessage, string propertyName)
{
ErrorMessage = errorMessage;
PropertyName = propertyName;
}
}
}



Part 3 - Controlling the data

We need to control the data for Stations and for the Fuel prices, remembering not all BioStations will have any fuel prices.

Start of by creating a new Controller and calling it BioStationController and select the default "Add action methods…"

Index ActionResult
In keeping with many lists we don't always want to list every available item. Therefore we need to provide a "Filter". To do this we need to add attributes to the method signature.

//
// GET: /BioStation/

public ActionResult Index(string county)
{

ViewData["Message"] = "All BioStations";

var stations = bioStationRepository.FindAllStations().ToList();

if (!string.IsNullOrEmpty(county))
stations = bioStationRepository.FindStationsByCounty(county).ToList();

if (stations == null || stations.Count == 0)
{
ViewData["Message"] = "There are no BioStations listed in " + county;
return View("NotFound");
}
else
return View("Index", stations);
}

There is no way to reach the filter unless we describe the route in the Global.asax.

routes.MapRoute(
"AllBioStations",
"BioStation/County/{county}",
new { controller = "BioStation", action = "Index", id = UrlParameter.Optional }
);

Details ActionResult
We need to alter the Details method to return a sngle BioStation Detail

//
// GET: /BioStation/Details/5

public ActionResult Details(int id)
{
ViewData["Message"] = "Full Details of BioStation";
BioStation station = bioStationRepository.GetStation(id);

if (station == null)
{
ViewData["Message"] = "BioStationID: " + id + " Not Found!";
return View("NotFound");
}
else
return View("Details", station);
}

Edit ActionResult

It may be necessary to edit a BioStation entry, we already have the stub made for the ActionResult Edit(int id). Now we need to do some work in the stub, but first we need to Add View. The is done in two part the first stub retrieves the record to be edited, the second stub updates the record.

//
// GET: /BioStation/Edit/5

public ActionResult Edit(int id)
{
ViewData["Message"] = "Edit Details of BioStation";
BioStation station = bioStationRepository.GetStation(id);

if (station == null)
{
ViewData["Message"] = "BioStationID: " + id + " Not Found!";
return View("NotFound");
}
else
return View("Edit", station);

}

The next part updates the record but we are not concerned with validation at this stage. Therefore we need to build the Update Logic.

//
// POST: /BioStation/Edit/5

[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
BioStation station = bioStationRepository.GetStation(id);
try
{

station.DateAdded = DateTime.Now;
UpdateModel(station);

return RedirectToAction("Details", new { id = station.StationID });
}
catch
{
return View();
}
finally
{
bioStationRepository.save();
}
}



Part Two to follow shortly ( or when I get some time)

No comments:

Post a Comment