Showing posts with label MVC. Show all posts
Showing posts with label MVC. Show all posts

25 November 2010

Adding a design mode to your MVC app

When developing websites you'll likely have ended up in the situation where you need to make some styling changes to a page that's buried deep within the site. If that page is at the end of a process such as registration or checkout then it can be extremely time consuming entering test data that passes validation in order to navigate to the correct page. Add to that the complexity of maybe needing to log in and also having to do the same thing on multiple browsers and things can get ridiculous. If you’re using the WebForms view engine then you have limited design time capability in Visual Studio but this isn’t satisfactory for ensuring cross-browser compatibility.

What's needed is a dumb version of the site which simply renders the views using a variety of data. Effectively you want to create a load of static pages, each with ViewData, Model etc set up so that they represent a different step in one of the real processes on the site. Using this version you’d be able to get to the correct page straight away, be able to refresh it quickly after making markup or CSS changes and be able to visit the page in all your test browsers. Ideally using this version of the site will require no authentication and it wont have any external dependencies like databases or web services that must be set up or configured.

We can use a set of different controllers to do this, each having some hard coded model data for example:

// A real controller may look like this... 
public class PeopleController : Controller
{
	public ActionResult Index()
	{
		List<Person> people = GetListOfPeopleFromDatabase();
		return View(people);
	}

	private List<Person> GetListOfPeopleFromDatabase()
	{
		// Do some data access
		
		return new List<Person>
			{
				new Person{ Name = "Runtime Person 1" },
				new Person{ Name = "Runtime Person 2" },
				new Person{ Name = "Runtime Person 3" },
			};
	}
}


// And our design time controller like this...
public class PeopleController : Controller
{
	[Description("Empty people list page")]
	public ActionResult EmptyList()
	{
		return View("Index", new List<Person>{});
	}

	[Description("People list page with 5 random people")]
	public ActionResult ListWithFivePeople()
	{
		return View("Index", new List<Person>
			{
				new Person
				{
					Name = "John Smith"
				},
				new Person
				{
					Name = "Betty Davis"
				},
				new Person
				{
					Name = "Steve Jobs"
				},
				new Person
				{
					Name = "Bill Gates"
				},
				new Person
				{
					Name = "John Carmack"
				},
			});
	}
}

This will work best if your model classes or, the data entity classes you're passing on to your views are dumb i.e. they don't try to do any database access when the view renders. If you already have your controllers in a separate assembly then it should be a relatively simple task to swap your design time ones in and use them instead. If however you have the standard MVC setup of controllers, views and models all in the same project and assembly then things are a bit more difficult.

At the very  least we want our design time controllers in a separate folder of our project, away from the real ones. The issue with this is that the default MVC controller factory will find them here anyway. Thankfully we don't need to implement an entire new factory, we can hide them from the default one by simply breaking with the convention it uses to identify them, the easiest way being not naming them "...Controller".

Home page

A nice to have in this "design" mode would be a default page which shows a list of links to all the actions of the design time controllers with descriptions for what each represents. This would be particularly useful when handing the markup and CSS over to a third party to be styled up as it allows them to quickly access each variation of each screen. You'd end up with something like this:

  • Products
    • List products
    • Search products
    • View product
    • Product category
  • Basket
    • Empty
    • Full
    • Saved
  • My Account
    • Addresses
    • Billing details
  • Home
  • Contact us

Variations

In addition to each individual view the design time functionality could also allow for variations of these pages e.g. logged in / logged out  views, special offer views, user customised views etc. Variations could be  defined on an action, a controller or on the whole site and rather than defining the particular data in each of these cases a transform function could be defined which is called before view render. This function could do work along the lines of setting IsAuthenticated booleans for the logged in / logged out case and possibly more complex operations otherwise.

This would allow a wide variety of viewable pages to be created without  needing to specifically define data in all those cases.

Proof of concept

I've put a quick proof of concept up on Github here:
https://github.com/dezfowler/MvcDesignMode

There's the main MvcDesignMode library and an example MVC app based on the standard template site which has few design time controllers named "...Designer" rather than "...Controller". When not in design mode this should prevent them ever being accidentally accessed provided you're using the default controller factory. I have the code to enable design mode in the App_Start of Global.asax.cs and it looks like this:

bool designMode = Convert.ToBoolean(ConfigurationManager.AppSettings["DesignMode"]);
if (designMode)
{
	DesignMode.Activate(typeof(HomeController));
}
else
{
	AreaRegistration.RegisterAllAreas();
	RegisterRoutes(RouteTable.Routes);
}

Here I'm just using a boolean configuration setting in web.config to turn the mode on and off but how you might choose to do it is up to you. If the design mode is activated the standard application startup stuff is skipped mainly because design mode uses a standard set of routes. Any links in your pages built using custom routes wont work correctly but the point of design mode isn't to be able to navigate around the site as normal it is that you can jump straight to a particular page in one click. I’m passing a type in to the Activate method simply to server as a pointer to the assembly where my design time controllers reside.

Once in design mode the design time controller factory hunts down the special controllers ending with "...Designer" and effectively indexes them pulling out action method names and also the text from a Description attribute defined on the methods. Using this index it builds up a special site map listing each controller and its action methods as links.

Conclusion

Have a look at the solution on Github or have a go implementing something similar yourself. On a number of recent projects I could see having a setup like this saving a lot of time and effort not just for styling and markup but probably developing simple JavaScript stuff as well. I'll definitely be using it myself in all my future MVC projects.

11 August 2010

Model binding and localization in ASP.NET MVC2

When creating an MVC site catering for different cultures, one option for persisting the culture value from one page to the next is by using an extra route value containing some form of identifier for the locale e.g.

/en-gb/Home/Index
/en-us/Cart/Checkout
/it-it/Product/Detail/1234

Here just using the Windows standard culture names based on RFC 4646 but you could use some other standard or your own custom codes. This method doesn’t rely on sessions or cookies and also has the advantage that the site can be spidered in each supported language.

Creating a base controller class for your site allows you to override one of its methods in order to set your current culture. For example if you amend your route configuration to "{locale}/{controller}/{action}/{id}" you could do the following:

string locale = RouteData.GetRequiredString("locale");
CultureInfo culture = CultureInfo.CreateSpecificCulture(locale);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;

It's important to set both CurrentCulture and CurrentUICulture as ResourceManager, used for retrieving values form localized .resx files, will refer to CurrentUICulture whereas most other formatting routines use CurrentCulture.

Once our culture is set, when we output values in our views ResourceManager can pick up our culture specific text translations from the correct .resx file and dates and currency values will be correctly formatted. String.Format("{0:s}", DateTime.Now), with "s" being the format string for a short date, will produce mm/dd/yyyy for en-US versus dd/mm/yyyy for en-GB.

This isn't the end of the story however, the problem arises of where in the controller do you perform your culture setting. It can't happen in the constructor because the route data isn't yet available so instead we could put it in an override of OnActionExecuting. This will seem to work fine for values output in your views but you come across a gotcha within model binding. Create a textbox in a form which binds to a DateTime and you'll end up with the string value being parsed using the default culture of the server. Using the US and UK dates example where your server's default culture is US but your site is currently set to UK. If you try to enter a date of 22/01/2010 you'll get a model validation error because it's being parsed as the US mm/dd/yyyy and 22 isn't a valid value for the month. Model binding happens before OnActionExecuting so that's no good.

A bit of digging around in Reflector and the Initialize method comes out as probably the best candidate for this as it is where the controller first receives route data and it occurs before model binding. We end up with something like (exception handling omitted for brevity):

protected override void Initialize(RequestContext requestContext) 
{
    base.Initialize(requestContext);
    string locale = RouteData.GetRequiredString("locale");
    CultureInfo culture = CultureInfo.CreateSpecificCulture(locale);
    Thread.CurrentThread.CurrentCulture = culture;
    Thread.CurrentThread.CurrentUICulture = culture;
 }

Both model binding and output of values will now be using the correct culture.

13 May 2008

Creating Views using XSL in ASP.NET MVC

There's something about Web Forms that never felt quite right to me; it all seemed a bit too much like a bodge that created more problems than it solved. I've been having a tinker with ASP.NET MVC for the past few days now and I'm really liking the way it all fits together; giving you clean separation between the layers and a means of passing data between them.

The View layer in MVC uses a very Classic ASP-esque markup for mixing code and HTML. Seems rather dirty but by this point any major processing should have been done and all that you'll need to do is render HTML with maybe some looping.

That being said if all we'll need to do by this point is some looping at the most then why not use an XSL transform instead?

XSL is well defined and established now with a load of tools out there for giving you a WYSIWYG view of your tranform as you build it. Plus as it is purely XML based it will likely be a lot easier for designers (the folk who we really want to build Views) to get to grips with.

A quick proof of concept

Step 1 - Create a model class for your XSL ViewData

This has simply an XmlDocument which will contain our serialized object and a string which will contain a path to our XSL file.

public class XslViewData
{
 private System.Xml.XmlDocument _doc;
 private string _xslPath;

 public XslViewData(System.Xml.XmlDocument doc, string xslPath)
 {
  _doc = doc;
  _xslPath = xslPath;
 }
  
 public System.Xml.XmlDocument Doc
 {
  get 
  {
   return _doc;
  }
 }

 public string XslPath
 {
  get
  {
   return _xslPath;
  }
 }
}

Step 2 - Serialize your object to XML in your Controller

First make sure the type you want to render is ready for XML serialization. This involves adding some attributes to classes and properties so read up on that first before you carry on. It's important to get your type serializing out in a nice way as it will be easier to work with in the XSL.

using System.Xml;
using System.Xml.Serialization;
...
Product prod = ProductRepository.GetProduct(id);
XmlSerializer ser = new XmlSerializer(typeof(Product));  
XmlDocument doc = new XmlDocument();
System.IO.MemoryStream ms = new System.IO.MemoryStream();
XmlWriter xw = XmlWriter.Create(ms);
ser.Serialize(xw, prod);
ms.Position = 0;
doc.Load(XmlReader.Create(ms));
RenderView("Xsl", new XslViewData(doc, "/Content/prodDetail.xsl"));

Step 3 - Create a ViewPage or ViewUserControl to host your XSL

All you need in the aspx/ascx file is an Xml ASP.NET control...

<asp:Xml ID="Xml1" runat="server" onload="Xml1_Load"></asp:Xml>

...and in the code behind...

public partial class Xsl : ViewPage<XslViewData>
{
 protected void Xml1_Load(object sender, EventArgs e)
 {
  Xml1.Document = ViewData.Doc;
  Xml1.TransformSource = ViewData.XslPath;
 }
}

Step 4 - Create an XSL file

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
 
 <xsl:template match="/Product">
  <h2><xsl:value-of select="Name"/></h2>
  <xsl:apply-templates select="Image"/>
    </xsl:template>

 <xsl:template match="Image">
  <img>
   <xsl:attribute name="src">
    <xsl:value-of select="ImageUrl"/>
   </xsl:attribute>
  </img>
 </xsl:template>
 
 <xsl:template match="*">
 </xsl:template>
 
</xsl:stylesheet>

Done

Not only is this simple to implement it has a lot of scope for improvment; compiling transforms, adding caching and configuation etc. This method also has the advantage that it requires no recompile to alter the template.

Links