eWorld.UI - Matt Hawley

Ramblings of Matt

I'm Twittering Now... I guess

May 29, 2008 23:04 by matthaw

I'm giving this a try, it's the "fad" thing now - yeah, I'm late to the game, I know. Oh well, let's see how that goes - probably just as good as my MySpace & FaceBook stints... I give it 1.5 months, and it'll be done! Anyway, if your in any way interested, start following me.



Categories: General | Personal
Actions: E-mail | Permalink | Comments (0) | Comment RSSRSS comment feed

Lambda Based RedirectToAction Sample Updated to MVC Preview 3

May 29, 2008 18:44 by matthaw

I took my Lambda based RedirectToAction solution I previously blogged about and updated it to work against the Preview 3 bits. I also took the liberty to fix the bug where you couldn't actually call another controller's action. Some notable changes in the source, is that ActionRedirectResult is no more - as it's replaced with RedirectToRouteResult. I think this was a good consolidation between RedirectToAction and RedirectToRoute since they basically do exactly the same thing. You'll also notice that there are 3 more extension methods, which was necessary to fix the prior bug. Now, you can write code like

   1:  // for actions off of the current controller
   2:  return this.RedirectToAction(c => c.Login());
   3:  return this.RedirectToAction(c => c.Login("matt"));
   4:   
   5:  // alternatively, within the UserController you can do the following
   6:  return this.RedirectToAction<ProductController>(c => c.View(102));

I'm not going to post the entire code sample as you can download it here. Enjoy it, and let me know of anything else you'd like to see.

kick it on DotNetKicks.com



Categories: .NET | ASP.NET | Development | MVC
Actions: E-mail | Permalink | Comments (9) | Comment RSSRSS comment feed

ViewData "dot" Notation Expressions in ASP.NET MVC

May 29, 2008 06:48 by matthaw

Here's something very cool I just found in the ASP.NET MVC Preview 3 bits, you can specify, what I call, "dot" notation expressions on your view data. Say you had the following model:

   1:  public class Bar {
   2:     public int Id { get; set; }
   3:     public string Name { get; set; }
   4:  }
   5:   
   6:  public class Foo {
   7:     public int Id { get; set; }
   8:     public string Name { get; set; }
   9:     public Bar TheBar { get; set; }
  10:  }

When you define your view, you specify Foo as the type of your model. If you cared about type safety prior to run-time, you'd probably write your code in your view like:

<%= Html.Encode(ViewData.Model.TheBar.Name) %>

However, with the new ViewDataDictionary you can now use the "dot" notation expressions to get access the same. As you'll see, you're simply using the indexer of your ViewData object, which internally performs a data-binding eval like operation to search for the key. It is operating in a way that it first will access any specific ViewData items set within your controller, and if that is not found will attempt to perform an eval against your the Model. Using the following controller action:

   1:  public class FooController : Controller {
   2:     public ActionResult View() {
   3:        Foo foo = new Foo() { Id = 1, 
   4:                              Name = "My Foo",
   5:                              TheBar = new Bar() {
   6:                                  Id = 12,
   7:                                  Name = "My Bar"
   8:                              }
   9:                            };
  10:        return View(foo);
  11:     }
  12:  }

You would write the following syntax in your view:

<%= Html.Encode(ViewData["TheBar.Name"]) %>

Given that you have not set the following,

ViewData["TheBar.Name"] = "My Bar";

within your code, you'll be surprised that this works like magic! However, if for any reason you do set the prior ViewData key in the controller action, it's value will be displayed instead of going to your model. Going even further, it doesn't necessarily always have to go off of your Model either, as

ViewData["FooObj"] = foo;

would allow you to access the members with the following expression within your view:

<%= Html.Encode(ViewData["FooObj.TheBar.Name"]) %>

Wow, super-powerful :) You gotta love that syntactical sugar!

kick it on DotNetKicks.com



ASP.NET MVC - Localization Helpers

May 16, 2008 15:58 by matthaw
In addition to blogging, I'm also using Twitter. Follow me @matthawley

 

Note: This post has been updated to work with MVC 2 RTM. The example code is a bit smelly (use POCOs, please!) but the localization helpers do correctly work with the WebFormViewEngine.

 

You're localizing your application right? Sure, I bet we ALL are - or at least, we're all storing our strings in resource files so that later we can localize. I know, I don't either :) but that doesn't mean if you're working on a large application that needs to be localized in many different languages, you shouldn't be thinking about it. While localization was possible in 1.0/1.1, ASP.NET 2.0 introduced us to a new expression syntax that made localization much easier, simply writing the following code

   1:  <asp:Label Text="<%$ Resources:Strings, MyGlobalResource %>" runat="server" />
   2:  <asp:Label Text="<%$ Resources:MyLocalResource %>" runat="server" />

Of course, you could always use the verbose way and call out to the HttpContext to get local and global resources, but I really enjoy writing the expression syntax much better as it truly implies that the code knows the context of your view / page. So, you could write both of the above examples like

   1:  <%= HttpContext.Current.GetGlobalResourceString("Strings", "MyGlobalResources",
   2:            CultureInfo.CurrentUICulture) %>
   3:  <%= HttpContext.Current.GetLocalResourceString("~/views/products/create.aspx", 
   4:            "MyLocalResource", CultureInfo.CurrentUICulture) %>

So now, you've started on that next big project and have been given the green light to use ASP.NET MVC, but ... your application needs to be localized in Spanish as well. In the current bits, there's really no way of using localized resources aside from (gasp!) using the Literal server control or the verbose method. But, you're moving to MVC to get away from the web forms model & nomenclature, so those are not an option any longer. Well, taking my earlier example of PRG pattern, I decided to "localize" it in an example of your project. First off, you'll need to create your global and local resources. Add a "App_GlobalResources" folder to the root. Add a Strings.resx file, and start to enter your text. Next, we'll add 2 local resources for our views. Under /Views/Products, create a "App_LocalResources", and 2 .resx files named "Create.aspx.resx" and "Confirm.aspx.resx".

 

Okay, now you're all set. Let's start converting our code to use the resources. You'll see that I'm using a new extension method (code will come later) in both the controller actions and in the view itself.

 

   1:  public class ProductsController : Controller
   2:  {
   3:      public ActionResult Create()
   4:      {
   5:          if (TempData["ErrorMessage"] != null)
   6:          {
   7:              ViewData["ErrorMessage"] = TempData["ErrorMessage"];
   8:              ViewData["Name"] = TempData["Name"];
   9:              ViewData["Price"] = TempData["Price"];
  10:              ViewData["Quantity"] = TempData["Quantity"];
  11:          }
  12:          return RenderView();
  13:      }
  14:   
  15:      public ActionResult Submit()
  16:      {
  17:          string error = null;
  18:          string name = Request.Form["Name"];
  19:          if (string.IsNullOrEmpty(name))
  20:              error = this.Resource("Strings, NameIsEmpty");
  21:   
  22:          decimal price;
  23:          if (!decimal.TryParse(Request.Form["Price"], out price))
  24:              error += this.Resource("Strings, PriceIsEmpty");
  25:   
  26:          int quantity;
  27:          if (!int.TryParse(Request.Form["Quantity"], out quantity))
  28:              error += this.Resource("Strings, QuantityIsEmpty");
  29:   
  30:          if (!string.IsNullOrEmpty(error))
  31:          {
  32:              TempData["ErrorMessage"] = error;
  33:              TempData["Name"] = Request.Form["Name"];
  34:              TempData["Price"] = Request.Form["Price"];
  35:              TempData["Quantity"] = Request.Form["Quantity"];
  36:              return RedirectToAction("Create");
  37:          }
  38:   
  39:          return RedirectToAction("Confirm");
  40:      }
  41:   
  42:      public ActionResult Confirm()
  43:      {
  44:          return RenderView();
  45:      }
  46:  }

Next, convert views over to use the new Resource extension method, below is the Create view:

   1:  <% Html.BeginForm("Submit"); %>
   2:    <% if (!string.IsNullOrEmpty((string)ViewData["ErrorMessage"])) { %>
   3:      <div style="color:red;"><%= ViewData["ErrorMessage"] %></div>
   4:    <% } %>
   5:    <%= Html.Resource("Name") %> <%= Html.TextBox("Name", ViewData["Name"]) %><br />
   6:    <%= Html.Resource("Price") %> <%= Html.TextBox("Price", ViewData["Price"]) %><br />
   7:    <%= Html.Resource("Quantity") %> <%= Html.TextBox("Quantity", ViewData["Quantity"]) %><br />
   8:    <input type="submit" value="<%= Html.Resource("Save") %>" />
   9:  <% Html.EndForm(); %>

Here's the Confirm view:

   1:  <%= Html.Resource("Thanks") %><br /><br />
   2:  <%= Html.Resource("CreateNew", Html.ActionLink<ProductsController>(c => c.Create(), 
   3:                             Html.Resource("ClickHere"))) %>

As you can see, I'm using a mixture of resource expressions both within the controller and view implementation. Here are the main implementations:

   1:  // default global resource
   2:  Html.Resource("GlobalResource, ResourceName")
   3:   
   4:  // global resource with optional arguments for formatting
   5:  Html.Resource("GlobalResource, ResourceName", "foo", "bar")
   6:   
   7:  // default local resource
   8:  Html.Resource("ResourceName")
   9:   
  10:  // local resource with optional arguments for formatting
  11:  Html.Resource("ResourceName", "foo", "bar")

As you can see, it supports both Global Resources and Local Resources. When working within your controller actions, only Global Resources work as we don't have a concept of a "local resource." The implementation for Html.Resource is actually a wrapper around the verbose method I previously mentioned. It does, however, take into consideration the expression syntax and the context of where the code is calling from to smartly determine the correct resource call to make. A gotcha in the codebase is that this code will only work with the WebFormViewEngine out of the box for local resources. The reason for this is that the code needs a way to find the associated virtual path for the view it's currently rendering, which is only available for the WebFormsView. Should you be using another View Engine, you'll have to modify the codebase to use derived IView type to find the virtual path. So, here's the code:

   1:  public static string Resource(this HtmlHelper htmlhelper, string expression, params object[] args)
   2:  {
   3:    string virtualPath = GetVirtualPath(htmlhelper);
   4:    return GetResourceString(htmlhelper.ViewContext.HttpContext, expression, virtualPath, args);
   5:  }
   6:   
   7:  public static string Resource(this Controller controller, string expression, params object[] args)
   8:  {
   9:    return GetResourceString(controller.HttpContext, expression, "~/", args);
  10:  }
  11:   
  12:  private static string GetResourceString(HttpContextBase httpContext, string expression, string virtualPath, object[] args)
  13:  {
  14:    ExpressionBuilderContext context = new ExpressionBuilderContext(virtualPath);
  15:    ResourceExpressionBuilder builder = new ResourceExpressionBuilder();
  16:   ResourceExpressionFields fields = (ResourceExpressionFields)builder.ParseExpression(expression, typeof(string), context);
  17:   
  18:    if (!string.IsNullOrEmpty(fields.ClassKey))
  19:      return string.Format((string)httpContext.GetGlobalResourceObject(fields.ClassKey, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
  20:   
  21:    return string.Format((string)httpContext.GetLocalResourceObject(virtualPath, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
  22:  }
  23:   
  24:  private static string GetVirtualPath(HtmlHelper htmlhelper)
  25:  {
  26:    WebFormView view = htmlhelper.ViewContext.View as WebFormView;
  27:   
  28:    if (view != null)
  29:      return view.ViewPath;
  30:   
  31:    return null;
  32:  }

And just so you know I'm not lying - here's the output in English and Spanish!

English Spanish

Since this example code is so lengthy, I've zipped up the main code to make things much easier for you to bring into your solution.

kick it on DotNetKicks.com



ASP.NET MVC: Expression Based RedirectToAction Method

May 14, 2008 21:29 by matthaw

Update: I've updated the source to be compatible with the Preview 3 bits. Download and execution is the same, but the sample code within the post is no longer valid.

Since the introduction of lambda expressions within the .NET framework, and it's extensive use of them within ASP.NET MVC, I've grown extremely fond of working with compile time errors that lambda expressions gives us. You've seen the ASP.NET MVC team build out a set of ActionLink<T> methods that enable you to specify an expression that will be compiled like the following

<%= Html.ActionLink<UserController>(c => c.Login(), "Login") %>

As the team released their April push and introduced us to ActionResult return types, I've totally fell in love with using these, mainly from a testability standpoint, and it also makes your code extremely readable to determine exactly what is going to be happening at what point. However, what I've found is that when using RedirectToAction, I would constantly be writing code like

   1:  public class UserController : Controller
   2:  {
   3:     public ActionResult Login() { ... }
   4:     public ActionResult ProcessLogin()
   5:     {
   6:        // ... determine if error'd
   7:        return RedirectToAction("Login");
   8:     }
   9:  }

Yup, nothing fancy and pretty straightforward. However, what if I needed to refactor my codebase and change my action method names... the task becomes straight forward when using ActionLink<T> in my views, but all of my controllers continue to compile even when they shouldn't be! Yes, if you're doing true TDD, this task is easy to spot because after refactoring, you can run all of your test cases and see which ones failed.

 

Great. That's a lot of manual work...mmmmkay. In the age of having refactoring shortcuts built right into the IDE, why couldn't I change my method name using refactoring, and have IT do all the work for me? Enter, the expression based RedirectToAction method. I know, you saw it coming, right? Here's how I want to write my above code

   1:  public class UserController : Controller
   2:  {
   3:     public ActionResult Login() { ... }
   4:     public ActionResult ProcessLogin()
   5:     {
   6:        // ... determine if error'd
   7:        return this.RedirectToAction(c => c.Login());
   8:     }
   9:  }

Ohh, pretty - and hey look, refactoring now works! It even supports parameters, route value dictionaries, anonymous types. Even better, you can specify a completely different controller to route to!

   1:  // parameters, route dictionaries, anonmyous types
   2:  this.RedirectToAction(c => c.Login("matt"));
   3:  this.RedirectToAction(c => c.Login(), new RouteValueDictionary(new { userName = "matt" }));
   4:  this.RedirectToAction(c => c.Login(), new { userName = "matt" }));
   5:   
   6:  // different controller
   7:  this.RedirectToAction<ProductsController>(c => c.View(101));

All this is super powerful, and I'm sure your dying to get your hands on the source...okay :)

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq.Expressions;
   4:  using System.Web.Mvc;
   5:  using System.Web.Routing;
   6:   
   7:  public static class ControllerExtensions
   8:  {
   9:      public static ActionRedirectResult RedirectToAction<T>(this T controller, 
  10:          Expression<Action<T>> action) where T : Controller
  11:      {
  12:          return RedirectToAction<T>(controller, action, null);
  13:      }
  14:   
  15:      public static ActionRedirectResult RedirectToAction<T>(this T controller, 
  16:          Expression<Action<T>> action, object values) where T : Controller
  17:      {
  18:          return RedirectToAction<T>(controller, action, new RouteValueDictionary(values));
  19:      }
  20:   
  21:      public static ActionRedirectResult RedirectToAction<T>(this T controller, 
  22:          Expression<Action<T>> action, RouteValueDictionary values) where T : Controller
  23:      {
  24:          MethodCallExpression body = action.Body as MethodCallExpression;
  25:          if (body == null)
  26:              throw new InvalidOperationException("Expression must be a method call.");
  27:   
  28:          if (body.Object != action.Parameters[0])
  29:              throw new InvalidOperationException("Method call must target lambda argument.");
  30:   
  31:          string actionName = body.Method.Name;
  32:          string controllerName = typeof(T).Name;
  33:   
  34:          if (controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
  35:              controllerName = controllerName.Remove(controllerName.Length - 10, 10);
  36:   
  37:          RouteValueDictionary parameters = LinkBuilder.BuildParameterValuesFromExpression(body);
  38:   
  39:          values = values ?? new RouteValueDictionary();
  40:          values.Add("controller", controllerName);
  41:          values.Add("action", actionName);
  42:   
  43:          if (parameters != null)
  44:          {
  45:              foreach (KeyValuePair<string, object> parameter in parameters)
  46:              {
  47:                  values.Add(parameter.Key, parameter.Value);
  48:              }
  49:          }
  50:   
  51:          return new ActionRedirectResult(values);
  52:      }
  53:  }

As you can see, it's fairly straight forward. This is something that I think is very useful, and gets more in line with how code should be written. Since this example is a bit lengthy, I've provided the source in downloadable format, so click here to get it. Hope you enjoy and use this in your applications!

kick it on DotNetKicks.com

ASP.NET MVC: UI Validation Framework Update

May 14, 2008 08:53 by matthaw

I've taken some recommendations regarding the MVC UI Validation framework I originally posted about, which is available within the MvcContrib project. I'm still working on future advancements like attributing your model, but I did make a significant step forward ensuring both client and server side validation is successful. With some simple changes to how you are generating your validators, you can easily achieve this new functionality. Let's use an example:

   1:  using MvcContrib.UI.Tags.Validators;
   2:   
   3:  public class UserController : Controller
   4:  {
   5:     public ActionResult Index()
   6:     {
   7:        return RenderView();
   8:     }
   9:   
  10:     public ActionResult Add()
  11:     {
  12:        return RenderView(new UserData());
  13:     }
  14:   
  15:     public ActionResult Save()
  16:     {
  17:        UserData data = new UserData();
  18:        BindingHelperExtensions.UpdateFrom(data, Request.Form);
  19:   
  20:        if (!BaseValidator.Validate(Request.Form, data.Validators))
  21:           return RenderView("Add", data); // You should follow PRG pattern
  22:   
  23:        // save the data
  24:   
  25:        return RedirectToAction("Index");
  26:     }
  27:  }
  28:   
  29:  public class ValidatorViewData
  30:  {
  31:     private IList<IValidator> validators = new List<IValidator>();
  32:   
  33:     public IList<IValidator> Validators
  34:     {
  35:        get { return validators; }
  36:     }
  37:  }
  38:   
  39:  public class UserData : ValidatorViewData
  40:  {
  41:     public UserData()
  42:     {
  43:        this.Validators.Add(new RequiredValidator("firstNameRequired", "firstName", 
  44:                "First Name is Required"));
  45:        this.Validators.Add(new RegularExpressionValidator("firstNameRegex", "firstName", 
  46:                "[a-zA-Z]*", "First Name can only contain letters."));
  47:        this.Validators.Add(new RequiredValidator("ageRequired", "age", "Age is required."));
  48:        this.Validators.Add(new RegularExpressionValidator("ageRegex", "age", "[0-9]*", 
  49:                "Age can only be numeric."));
  50:        this.Validators.Add(new RangeValidator("ageRange", "age", "18", "35", 
  51:                ValidationDataType.Integer, "Age is not in the range of 18-35."));
  52:     }
  53:   
  54:     public string FirstName { get; set; }
  55:     public string Age { get; set; }
  56:  }

In this controller and view data, we see that we're generating our validators within the view data object. This enables us to validate the Request.Form collection against our list of validators as seen in line 20 above. This works because when we created each of our validators, we gave it the name of the form element we're expecting the value to be present in. When taking it to our view implementation, we'll use a new method to register our validators:

   1:  <%@ Import Namespace="System.Web.Mvc" %>
   2:  <%@ Import Namespace="MvcContrib.UI.Html" %>
   3:  <html>
   4:     <head>
   5:        <%= Html.Validation().ValidatorRegistrationScripts() %>
   6:     </head>
   7:     <body>
   8:        <% using (Html.Form<UserController>(c => c.Save(), Formmethods.Post, 
   9:              Html.Validation().FormValidation())) { %>
  10:        First Name: <%= Html.TextBox("firstName", this.ViewData.FirstName) %>
  11:        <%= Html.Validation().ElementValidation(this.ViewData.Validators, "firstName");
  12:        <br />
  13:        Age: <%= Html.TextBox("age", this.ViewData.Age) %>
  14:        <%= Html.Validation().ElementValidation(this.ViewData.Validators, "age");
  15:        <br />
  16:        <%= Html.SubmitButton("submit", "Save") %>
  17:        <% } %>
  18:     </body>
  19:  </html>

What you'll notice from the original is that we now have an ElementValidation extension method. This enables you to specify the list of validators you would like to be rendered at this time. You have the option of specifying the field name to filter the list by when rendering as well. Should any validators fail upon the server, and you re-render the view, those validators will initially be displayed. All in all, the approach is getting closer to what it needs to be. I'll be working further on the following features to really bring this to be a first class citizen:

  1. Move the Validate method from BaseValidator into a Controller extension method.
  2. Define a set of attributes that you can apply to your model's properties that will work with this validation model.

A few things to note, is that you can still explicitly use individual validators, but note that they're no longer off of Html.Form() but Html.Validation() now. Lastly, while these changes have been committed to the MvcContrib trunk, they're still not available in any publicly released builds. You'll have to download the source and compile the binaries yourself until MvcContrib does another official build. Let me know what you think, and yes - I know, some people still don't like this approach :) It's not meant for everyone, and there's other projects out there that you can use. Enjoy, and let me know what you think of the progress.

kick it on DotNetKicks.com



ASP.NET MVC - Extracting Web Resources

May 13, 2008 11:40 by matthaw

One nice feature that ASP.NET added in the 2.0 feature set was the ability to embed resources directly within an assembly and then later extract them through a separate Http Handler. You've notably saw this by all the WebResource.axd calls. If you're building a ASP.NET MVC view, you have two ways of extracting web resource urls.

Way 1

If you are using the default view engine, all of your views should derive either from ViewPage or ViewUserControl, which ultimately has access to ClientScript.WebResourceUrl, etc. This method is the easiest to use as it doesn't require anything special:

<script src="<%= ClientScript.WebResourceUrl(typeof(Foo), "Foo.Bar.js") %>" type="text/javascript">
</script>

This way works great, as I said if you're using the WebFormsViewEngine and you're within the view itself. However sometimes you have a controller action or other html helper extension methods that are rendering HTML directly ... or you're just using a completely different view engine. At this point, you don't have access to ClientScript.

Way 2

The second way is by having your own extension method that will generate the web resource url for you. This way, you can get a web resource url anywhere you have an instance of your HtmlHelper object.

   1:  public static class HtmlHelperExtensions
   2:  {
   3:     private static MethodInfo getWebResourceUrlMethod;
   4:     private static object getWebResourceUrlLock = new object();
   5:   
   6:     public static string WebResourceUrl<T>(this HtmlHelper htmlHelper, string resourceName)
   7:     {
   8:        if (string.IsNullOrEmpty(resourceName)
   9:           throw new ArgumentNullException("resourceName");
  10:   
  11:        if (getWebResourceUrlMethod == null)
  12:        {
  13:           lock (getWebResourceUrlLock)
  14:           {
  15:              if (getWebResourceUrlMethod == null)
  16:              {
  17:                 getWebResourceUrlMethod = typeof(AssemblyResourceLoader).GetMethod(
  18:                                                  "GetWebResourceUrlInternal",
  19:                                                  BindingFlags.NonPublic | BindingFlags.Static);
  20:              }
  21:           }
  22:        }
  23:   
  24:        return "/" + (string) getWebResourceUrlMethod.Invoke(null,
  25:                         new object[] { Assembly.GetAssembly(typeof(T)), resourceName, false });
  26:     }
  27:  }

And that's it. Now, in your code you can get a web resource url very similarly as with ClientScript:

<script src="<%= Html.WebResourceUrl<Foo>("Foo.Bar.js") %>" type="text/javascript">
</script>

What I don't like about this approach is that it is using reflection to invoke the private method, but considering these methods are not exposed anywhere (ohh, and you cannot create an instance of ClientScriptManager) this is the only approach that can be taken. Furthermore, remember this is an ASP.NET feature, and works very nicely within the MVC framework. The url, WebResource.axd, is mapped to the AssemblyResourceLoader Http Handler which is independent of the ASP.NET web forms runtime. Hope this helps some people!

kick it on DotNetKicks.com



Categories: .NET | ASP.NET | Development | MVC
Actions: E-mail | Permalink | Comments (8) | Comment RSSRSS comment feed

ASP.NET MVC - Living in a Web Forms World

May 9, 2008 08:59 by matthaw

For the life of me, I seem to have Journey stuck in my head when I was thinking about writing this post, and as such the title reflects that! Anyway, onto the post...

 

When developing ASP.NET MVC applications, most examples or sites have shown you starting from complete scratch. This is all and well if you have the time to completely re-write an existing application for 6+ months or have started a v1 product. Right, in the "real-world" the former rarely happens and if your a developer wanting to stay on the bleeding edge, introducing a new architecture into an existing ASP.NET application is fairly tedious. From converting existing pages, supporting legacy routes, and possibly the lack of allowing multiple forms all prove to be a challenge.


Layout

As you know, I'm currently working on the CodePlex team and my biggest task I'm working on is slowly converting the site to use ASP.NET MVC. If you've been to this site, you know how extensive the site is, and how this challenge is very daunting. The one saving grace is that we can do it gradually since we release so often. But, because of this we have to take small steps converting pages (views!) that we can - while maintaining our original master page and other postback functionality. For this post, I'll use CodePlex as a reference point.

 

We have 2 major sections that we're to worry about while working with ASP.NET MVC. Most importantly, we still have a single form on the page! This is a critical piece of information, because this limitation required us to make decisions we did to get around ASP.NET MVC within a Web Forms world. When working on a MVC view, we render everything within a main content section, and do not use ASP.NET server controls at all. The site also contains a project search on each page but this, however, still relies upon that singe form and the normal Postback model.

 

Views

Like any web application, you have content pages and you have forms based pages. CodePlex is no different. When converting each of these types, obviously, the former of the two is the easiest to convert. Why? Well, you don't have to worry about that single form tag and there's no need of posting data back to the server. So, when working with true content pages - converting them is as simple as creating your controller, action, and view. Now, just because you're still in a Web Forms model, still does not give you the right to continue to use ASP.NET server controls!

 

The most challenging portion is working with forms based pages. If you were building a new ASP.NET MVC application from the ground-up, you wouldn't have to think twice about this problem. The reason is that when you create your forms based pages, each logical form has it's own form! Astounding, isn't it. Yes, we've been living in that world of ASP.NET Web Forms world wherein you can have 1 and only 1 form tag (with a runat="server"). Since the application still has this single form tag in place, and we still need to respond to master page events (like project searches) we have to be very mindful of how its done. The biggest challenge is that, if you know the ASP.NET MVC lifecycle, it goes

  1. HTTP Request arrives
  2. Routing HTTP Handler executes, matching based on routes
  3. MVC Route Handler executes controller & action
  4. The ViewPage is rendered (assuming your calling RenderView)
  5. The ASP.NET page life cycle is invoked (assuming your using the default view engine)

Do you see the problem? No? Okay, well its the fact that upon a HTTP Post, your Action method is invoked prior to any event occurring within the ASP.NET lifecycle. That means, at an Action method level - you have absolutely no idea if your MVC form was submitted or was it that pesky Project search. Well, this isn't entirely true because how to get around this is by having a hidden input field in which you set a value when your MVC based form is submitted. I'll show a little later how this is used, but you can probably figure out how.

   1:  <input type="hidden" id="mvcFormSubmitted" name="mvcFormSubmitted" value="" />
   2:  <label for="UserName">User Name:</label><br />
   3:  <%= Html.TextBox("UserName", ViewData["UserName"]) %><br />
   4:  <label for="Password">Password:</label><br />
   5:  <%= Html.Password("Password") %><br />
   6:  <%= ViewData["ErrorMessage"] %><br />
   7:  <%= Html.SubmitButton("submitButton", "Login", new { onclick = "mvcFormSubmit(); " }) %>
   8:   
   9:  <script type="text/javascript">
  10:  function mvcFormSubmit() {
  11:     $get('mvcFormSubmitted').value = 'true';
  12:  }
  13:  </script>

Routing

As you can imagine, routing has become a bit of a nightmare. Why? Well, since we still have to abide by the Postback model, our Views always postback to themselves. For example, /site/login will post to /site/login vs. /site/login posting to /site/processLogin. At first glance, you might think "okay, I'll just check to see if the http method is of type POST within my login action." That would work, but the correct method, which supports later migration away from a Web Forms world, is to have two separate actions. To handle this scenario, you simply use constraints on the HttpMethod within your route definition:

   1:  routes.MapRoute("GET Login", "/site/login", 
   2:                  new { controller = "session", action = "login" }
   3:                  new { httpmethod = "GET" });
   4:   
   5:  routes.MapRoute("POST Login", "/site/login",
   6:                 new { controller = "session", action = "processLogin" }
   7:                 new { httpmethod = "POST" }

Now, when your clients request /site/login, the login action will execute, and upon a POST to /site/login, the processLogin action will execute. Another scenario you should consider at this time, is that if you are converting a previous page, say "/Users/Login.aspx" you would want all subsequent requests to be routed to /site/login. To handle this, you can utilize my Legacy Routing methodology that would send a 301 http status code pointing the client to the new url.

   1:  routes.Add("", new LegacyRoute(
   2:     "Users/Login.aspx",
   3:     "GET Login",
   4:     new LegacyRouteHandler());

Controller Actions

Now it's time to put these two things together. Granted, if you were doing true TDD development, you would have started with your controller & actions, but for the purposes of this post I left it at the very end to show how everything comes together. So a few things to remember, is that we are now assured that our login and processLogin action will execute at the correct time. We also have a hidden input field in which we can determine if it was my form submission that caused the postback, or was it some other control on the master page. So, here's how things should look like:

   1:  public class SessionsController : Controller
   2:  {
   3:     public ActionResult Login()
   4:     {
   5:        if (TempData["ErrorMessage"] != null)
   6:        {
   7:            ViewData["UserName"] = TempData["UserName"];
   8:        }
   9:        return RenderView();
  10:     }
  11:   
  12:     public ActionResult ProcessLogin()
  13:     {
  14:        if (Request.Form["mvcFormSubmitted"] != "true")
  15:           return RenderView("Login");
  16:   
  17:        string userName = Request.Form["UserName"];
  18:        string password = Request.Form["Password"];
  19:        if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
  20:        {
  21:           TempData["ErrorMessage"] = "Username and Password not valid.";
  22:           TempData["UserName"] = userName;
  23:           return RedirectToAction("Login");
  24:        }
  25:   
  26:        // process login
  27:        return Redirect("~/home");
  28:     }
  29:  }

As example, I've followed the PRG pattern that I've previously mentioned, but there's one thing here that breaks that pattern. It's the RenderView("Login") if my hidden field does not match. Why do we need to break the pattern here? Well remember the lifecycle I previously mentioned. The only way for events to be fired from your master page, is that it too needs to "postback", thus falling back into the postback model. Once you can safely ditch the singe form, that code can be safely removed.

 

Final Thoughts

As you can tell, having a mixture of ASP.NET WebForms and MVC forms is not a trivial task. There's a lot to consider when tackling this, and if at all possible SPIKE! Yes, spike your master pages so that you can have a Web Forms master page AND a MVC master page. By doing this, you do not have to follow the single form pattern thus negating almost all this post. I would still recommend using the legacy routing to redirect your users to the new page as well as the PRG pattern, but you should always be doing that anyway :) Well, hopefully this post has served its purpose and given you some ideas on how to approach converting existing ASP.NET applications to ASP.NET MVC. If you have any other suggestions or better recommendations, please add a comment!

 

kick it on DotNetKicks.com



ASP.NET MVC - Using Post, Redirect, Get Pattern

May 8, 2008 23:51 by matthaw
In addition to blogging, I’m also using Twitter. Follow me @matthawley

 

Note: This post has been updated to work with MVC 2 RTM. The example code is a bit smelly (use POCOs, please!) but the workflow is what you should be mostly concerned about. 

imageThe ASP.NET MVC pattern tends to lead itself into a more simplified and "true" HTTP experience by re-introducing  patterns that have been lost, or at least, not followed in many years. One such pattern is the Post, Redirect, Get (PRG) pattern in which it is "to help avoid duplicate form submissions and allow web applications to behave more intuitively with browser bookmarks and the reload button" (Wikipedia).

 

A normal ASP.NET Web Form Lifecycle has the following pattern

  1. HTTP GET of "Create.aspx"
  2. HTTP POST of "Create.aspx"
  3. Validation Fails, "Create.aspx" is Re-Rendered
  4. HTTP POST of "Create.aspx"
  5. Item is created, "Create.aspx" is Re-Rendered with confirmation message

The major problems with this Postback pattern, is that hitting the Refresh button of your browser in steps 3 or 5 will re-post your submitted data. Step 5 is more of a problem as it could possibly re-submit that created information. Granted, there are steps that you can take to approach this problem, but this is how default ASP.NET Web Forms are treated.

Taking this same approach within ASP.NET MVC, can be achieved in the same manner by rendering a your "Create" view from your POST action. For example:

  1. HTTP GET of "/products/create", "Create" view is rendered
  2. HTTP POST to "/products/submit"
  3. Validation Fails, "Create" view is rendered
  4. HTTP POST to "/products/submit"
  5. Item is created, "Confirm" view is rendered

As you'll notice, the same problems we had with ASP.NET Web Forms exists with ASP.NET MVC. The really nice option, is that ASP.NET MVC gives you a lot more "freedom" of how the workflow is processed. If we strictly follow the PRG pattern within ASP.NET MVC, it would look something like

  1. HTTP GET of "/products/create", "Create" view is rendered
  2. HTTP POST to "/products/submit"
  3. Validation Fails, redirect to "/products/create", "Create" view is rendered
  4. HTTP POST to "/products/submit"
  5. Item is created, redirect to "/products/confirm", "Confirm" view is rendered

As you'll notice, where we previously could have had issues in step 3 or 5 before, we no longer have issues. If a user presses the Refresh button in either of those steps, they'll not get the lovely "Would you like to resubmit the form data" confirmation as featured below - instead, the page just reloads.

image

To implement this, you'll need 1 controller, 3 action methods, and 2 views. Follow the steps below to achieve this pattern:

   1:  using System.Web.Mvc;
   2:   
   3:  public class ProductsController : Controller
   4:  {
   5:     public ActionResult Create() { ... }
   6:     public ActionResult Submit() { ... }
   7:     public ActionResult Confirm() { ... }
   8:  }

When you implement your Create action, you have to keep in mind that validation may fail and you may need to re-display the form. TempData is best suited for this scenario, and is implemented as such.

 

   1:  public ActionResult Create()
   2:  {
   3:     if (TempData["ErrorMessage"] != null)
   4:     {
   5:        ViewData["ErrorMessage"] = TempData["ErrorMessage"];
   6:        ViewData["Name"] = TempData["Name"];
   7:        ViewData["Price"] = TempData["Price"];
   8:        ViewData["Quantity"] = TempData["Quantity"];
   9:     }
  10:     return View();
  11:  }

Next you'll implement your Submit action. This will perform some validation of the user input data, and if successful will save the info and redirect to the Confirm action. If it is not successful, we'll store the form data into the TempData and redirect to the action Create. This way we mimic maintaining the view's state even if it fails.

   1:  public ActionResult Submit()
   2:  {
   3:      string error = null;
   4:      string name = Request.Form["Name"];
   5:      if (string.IsNullOrEmpty(name))
   6:      {
   7:          error = "Name is empty. ";
   8:      }
   9:      decimal price;
  10:      if (!decimal.TryParse(Request.Form["Price"], out price))
  11:      {
  12:          error += "Price is invalid. ";
  13:      }
  14:      int quantity;
  15:      if (!int.TryParse(Request.Form["Quantity"], out quantity))
  16:      {
  17:          error += "Quantity is invalid.";
  18:      }
  19:   
  20:      if (!string.IsNullOrEmpty(error))
  21:      {
  22:          TempData["ErrorMessage"] = error;
  23:          TempData["Name"] = Request.Form["Name"];
  24:          TempData["Price"] = Request.Form["Price"];
  25:          TempData["Quantity"] = Request.Form["Quantity"];
  26:          return RedirectToAction("Create");
  27:      }
  28:      else
  29:      {
  30:          return RedirectToAction("Confirm");
  31:      }
  32:  }

Something very interesting to note in the above example, is that even though I've pulled all values out of the form into local variables, should either Price or Quantity fail in parsing and I set the TempData to the local variables...I would have lost the user input. So, it's always a smart idea to retrieve the data from the form directly into the TempData. Finally, the Confirm action needs to be implemented.

   1:  public ActionResult Confirm()
   2:  {
   3:      return View();
   4:  }

Now, it's time to create our views:

~/Views/Products/Create.aspx

   1:  <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Create.aspx.cs" Inherits="Views_Products_Create" %>
   2:  <%@ Import Namespace="System.Web.Mvc.Html" %>
   3:  <html xmlns="http://www.w3.org/1999/xhtml">
   4:  <head runat="server">
   5:      <title>Create Product</title>
   6:  </head>
   7:  <body>
   8:      <% using (Html.BeginForm("Submit", "Products")) { %>
   9:      <% if (!string.IsNullOrEmpty((string) ViewData["ErrorMessage"])) { %>
  10:          <div style="color:Red;">
  11:              <%= ViewData["ErrorMessage"] %>
  12:          </div>
  13:      <% } %>
  14:      Name: <%= Html.TextBox("Name", ViewData["Name"]) %><br />
  15:      Price: <%= Html.TextBox("Price", ViewData["Price"]) %><br />
  16:      Quantity: <%= Html.TextBox("Quantity", ViewData["Quantity"]) %><br />
  17:      <input type="submit" value="Save" />
  18:      <% } %>
  19:  </body>
  20:  </html>

~/Views/Products/Confirm.aspx

   1:  <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Confirm.aspx.cs" Inherits="Views_Products_Confirm" %>
   2:  <html xmlns="http://www.w3.org/1999/xhtml">
   3:  <head id="Head1" runat="server">
   4:      <title>Confirm Create Product</title>
   5:  </head>
   6:  <body>
   7:      Thanks for creating your product. 
   8:      <a href="<%= Url.Action("Create") %>">Click here</a> to create a new one.
   9:  </body>
  10:  </html>

And that's it. As you can see from the Create view, when writing our textboxes, we give them a default value from the ViewData. You can download the sample application with this pattern running here. Please let me know of any suggestions or issues.

kick it on DotNetKicks.com





Copyright © 2000 - 2024 , Excentrics World