Tuesday, 19 October 2010

Experimenting with MVC and the Entity Framework – Part 3 – Creating the comments.

This follows on from part 2.

So now to add in the creation of comments. Having read through the CRUD implementation in Nerddinner and using partials again, I decided to go straight for a partial for my comments creation, as I would almost certainly require an edit and maybe a view details too.

[more]

clip_image002

So I first created a partial using the add view option and then added a create page – again using the add view.

clip_image004

But this time the view was not strongly typed and empty.

I then set the Create View to display the partial

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">

Create

</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<h2>Create</h2>

<% Html.RenderPartial("CommentForm"); %>

</asp:Content>

A first bash at the post.

I new this wouldn’t work due to the FC constraint on the comments table requiring a venue – but I wanted to see what would happen:

[HttpPost]

public ActionResult Create(FormCollection collection)

{

Comment comment = new Comment();

try

{

UpdateModel<Comment>(comment);

victorEntities ve = new victorEntities();

ve.AddToCommentSet(comment);

ve.SaveChanges();

return RedirectToAction("Index");

}

catch

{

return View();

}

}

As expected – it blew up but due to the way this is coded – ie the catch “swallows” all exceptions – no error is presented to the user!

In my opinion this is an abuse of exceptions. Exceptions should not be used for normal program flow as:

1) When you are calling a method you have no way of determining what exceptions it throws/you need to handle (unlike java where you must explicitly declare the exceptions thrown – althogh this is not great as it forces the calling code to explicitly handle those exceptions in some way even if they can’t or declare that they throw them).

2) Exceptions should be just that – Exceptions. Business logic should be explicitly handled by code. In this case lack of input is definitely expeced. Exceptions should be for things beyond your control such as a file dissapearing or running out of memory.

My preference is always to handle exceptions in a global manner (usualy in the global.asax) – basically displaying something nice to the user and logging all the details you can. In theory we should never get any but they may happen due to

· Bugs

· Hardware problems such as networking errors.

· Scaleability issuses

You can then monitor these in test and production and take necesasry.

I will add error logging at a later date.

So I re-engineered the method to

[HttpPost]

public ActionResult Create(FormCollection collection)

{

ActionResult ret = View();

Comment comment = new Comment();

if(TryUpdateModel<Comment>(comment))

{

victorEntities ve = new victorEntities();

ve.AddToCommentSet(comment);

ve.SaveChanges();

ret = RedirectToAction("Index");

}

return ret;

}

Notice – I also added in a single exit point – makes the code much easier to read and helps reduce resources left open (such as files etc.).

Adding in the venue.

A comment is not valid without a venue which must also have a type.

The Nerd Dinner sample does not cover aggregates – they never do – so I thought I’d just have a stab at it anyway.

Firstly I created an empty object in the GET

//

// GET: /Comments/Create

public ActionResult Create()

{

Comment ncomment = new Comment();

Venue venue = new Venue();

ncomment.Venue = venue;

return View(ncomment);

}

And then following the instructions on the ObjectContext.AddObject Method I changed the POST method to add the venue first to the entities first:

//

// POST: /Comments/Create

[HttpPost]

public ActionResult Create(FormCollection collection)

{

ActionResult ret = View();

Comment comment = new Comment();

if(TryUpdateModel<Comment>(comment))

{

victorEntities ve = new victorEntities();

ve.AddToVenueSet(comment.Venue );

ve.AddToCommentSet(comment);

ve.SaveChanges();

ret = RedirectToAction("Index");

}

return ret;

}

Finally I added in the html on the form for the venue name to test it.

<div class="editor-label">

<%= Html.LabelFor(model => model.Venue.vcrName) %>

</div>

<div class="editor-field">

<%= Html.TextBoxFor(model => model.Venue.vcrName) %>

<%= Html.ValidationMessageFor(model => model.Venue.vcrName)%>

</div>

I checked in the debugger and the name was correctly set to what was entered. An exception was thrown that the foreign key constraint to VenueType was broken – which was expected.

Adding a venue type dropdown.

clip_image006

Again – I looked through the NerdDinner sample and decided that in MVC speak I needed to either use ViewData or a view model.

ViewData involves creating a SelectList in the controller –

[Authorize]

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    ViewData["Countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);

    return View(dinner);

}

and displaying it in the View.

<%= Html.DropDownList("Country", ViewData["Countries"] as SelectList) %>

A view model involves creating a new Class based on the items you want to display.

The ViewData appears to be more of a “quick and dirty” solution that is not type safe and requires the setting of the view data in multiple places – e.g. both the get and the post.

So I opted to do the right thing – a philosophy that I’ve found has always worked well!

Whilst doing this I learn’t something new. Visual Studio 2005 featured a code snippet for properties which was fantastic. However, it always seemed broken in 2008. Since I had a bit of time I decided to try and get it back only to discover I no longer needed to declare my class attribute as it would be automatically generated. If I wanted my getter or setter to do something I can always change the code to a specific “backing” store (such as an attribute) later. This is called “Automatic Properties” – this guy was also looking for his code snippet but this lead me to this nice blog on automatic properties.

Here’s what I did to get it all working –

1) Created a new model class specific to the data I was modelling.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using System.Web.Mvc;

namespace victor.Models

{

public class CommentFormViewModel

{

/// <summary>

/// The venue types to display.

/// </summary>

public SelectList VenueTypes { get; set; }

/// <summary>

/// The view comment

/// </summary>

public Comment ViewComment { get; set; }

public CommentFormViewModel(Comment comment)

{

victorEntities ve = new victorEntities();

ViewComment = comment;

VenueTypes = new SelectList(ve.VenueTypeSet.ToList<VenueType>(),

"intVenueTypeId",

"vcrName",

comment.Venue.Type);

}

public CommentFormViewModel()

{

}

}

}

2) Changed the View Form to display extra attributes and inherit from the new model class:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<victor.Models.CommentFormViewModel>" %>

<% using (Html.BeginForm()) {%>

<%= Html.ValidationSummary(true) %>

<fieldset>

<legend>Fields</legend>

<div class="editor-label">

<%= Html.LabelFor(model => model.ViewComment.Venue.Type.intVenueTypeId ) %>

</div>

<div class="editor-field">

<%= Html.DropDownListFor(model => model.ViewComment.Venue.Type.intVenueTypeId ,Model.VenueTypes ) %>

<%=Html.ValidationMessageFor(model => model.ViewComment.Venue.Type)%>

</div>

<div class="editor-label">

<%= Html.LabelFor(model => model.ViewComment.Venue.vcrName) %>

</div>

<div class="editor-field">

<%= Html.TextBoxFor(model => model.ViewComment.Venue.vcrName)%>

<%= Html.ValidationMessageFor(model => model.ViewComment.Venue.vcrName)%>

</div>

3) Updated my create model to return the new Model

// GET: /Comments/Create

public ActionResult Create()

{

Comment ncomment = new Comment();

Venue venue = new Venue();

ncomment.Venue = venue;

CommentFormViewModel model = new CommentFormViewModel(ncomment);

return View(model);

}

4) Updated my create post method to use the new model, set the venue type based on the ID and complete some other mandatory fields to make the whole thing work.

// POST: /Comments/Create

[HttpPost]

public ActionResult Create(FormCollection collection)

{

ActionResult ret = View();

CommentFormViewModel fvm = new CommentFormViewModel();

if(TryUpdateModel<CommentFormViewModel>(fvm))

{

victorEntities ve = new victorEntities();

fvm.ViewComment.dteCreated = DateTime.Now;

fvm.ViewComment.dteLastUpdated = DateTime.Now;

fvm.ViewComment.vcrUserUpdated = HttpContext.User.Identity.Name;

fvm.ViewComment.Venue.dteCreated = DateTime.Now;

fvm.ViewComment.Venue.dteLastUpdated = DateTime.Now;

fvm.ViewComment.Venue.vcrUserUpdated = HttpContext.User.Identity.Name;

// set the venue type to an existing one

fvm.ViewComment.Venue.Type = ve.VenueTypeGetByID(

fvm.ViewComment.Venue.Type.intVenueTypeId).ElementAt<VenueType>(0);

ve.AddToVenueSet(fvm.ViewComment.Venue );

ve.AddToCommentSet(fvm.ViewComment);

ve.SaveChanges();

ret = RedirectToAction("Index");

}

return ret;

}

I wasn’t particularly happy with this solution as I new I’d be repeating code in my Edit method if and when I get one.

I will try and work out a better solution later on. See Concerns below.

Things left to do

I feel this is getting close to a beta release now. There are three things I want to do before I release the first version:

1) Introduce authentication/registration to be able to post comments.

2) Search for an existing venue when the user adds in a comment

3) Replace the CRUD operations with stored procedures.

4) Tidy up the fields to only show those required.

5) Carry out a security review

6) Investigate producing some unit tests.

Conclusions

  • Do not use the boiler plate code as the exception handling is very dodgy! Specifically it will “swallow” all your exceptions.
  • Use a specific model for your form to model behaviour or data types specific to your view.

Concerns

· One of MVC’s alleged advantages is it “It makes it easier to manage complexity by dividing an application into the model, the view, and the controller”. Personally – I always separate out the UI and business layer anyway and split the business layer into Business Process objects, Business Objects and a Data Access layer. It would seem to me that the Entity Framework has combined my DAL and Business objects and the processes are residing in my model. My concern is that these processes by default have been created as part of the User Interface, and not as a general API in a separate assembly as I would normally do. This will, IMO, make these harder to reuse for example, when I want to expose my business processes (the controller) as web services.

· There seems to be some strange stuff going on in the UI –

<div class="editor-label">

<%= Html.LabelFor(model => model.ViewComment.Venue.vcrTown) %>

</div>

<div class="editor-field">

<%= Html.TextBoxFor(model => model.ViewComment.Venue.vcrTown)%>

<%= Html.ValidationMessageFor(model => model.ViewComment.Venue.vcrTown)%>

</div>
I need to try and understand what this “model => model.ViewComment.Venue.vcrTown” is doing and how. I’ve a feeling its something called Lambda expressions.

Next up – authentication & authorisation

No comments:

Post a Comment