eWorld.UI - Matt Hawley

Ramblings of Matt

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



ASP.NET MVC - Legacy Url Routing

April 25, 2008 00:47 by matthaw
In addition to blogging, I'm also using Twitter. Follow me @matthawley

Recently, we've been converting over a lot of our ASP.NET Web Form pages to use ASP.NET MVC. While this is no small feat by itself, the underlying problem of having a new Url structure in the site while still supporting legacy Url's was necessary. The idea, is that you hit a page that no longer exists, and you get redirected to the appropriate controller & action within MVC.

 

Workflow

  1. A legacy Url is requested from your site. For example, http://www.server.com/Users/Login.aspx
  2. ASP.NET routing intercepts the request and matches a route from your route collection
  3. Instead of using the MvcRouteHandler, a LegacyRouteHandler is invoked.
  4. Using the LegacyRouteHandler, it'll use the route redirection name you specified, generate the MVC Url, and issue a HTTP 301 with the location of http://www.server.com/site/login.

Routing

First, we should define our legacy route class. This is necessary because we need to expose an additional property to enable our routing handler to find the correct MVC route.

   1: // The legacy route class that exposes a RedirectActionName
   2: public class LegacyRoute : Route {
   3:     public LegacyRoute(string url, string redirectActionName, IRouteHandler routeHandler)
   4:         : base(url, routeHandler)
   5:     {
   6:         RedirectActionName = redirectActionName;
   7:     }
   8:  
   9:     public string RedirectActionName { get; set; }
  10: }

Secondly, we need to define the route handler and associated http handler. The route handler derives from IRouteHandler, and will be the class used when creating your legacy routing. The http handler derives from MvcHandler because it gives us some critical information, like RequestContext. You'll also notice that (while not in the code) you need to copy all of the querystring parameters from the request over. This is a necessary step because the GetVirtualPath method call will take all route data (from RouteData.Values) and try and utilize that when building the Url itself.

   1: // The legacy route handler, used for getting the HttpHandler for the request
   2: public class LegacyRouteHandler : IRouteHandler {
   3:     public IHttpHandler GetHttpHandler(RequestContext requestContext) {
   4:         return new LegacyHandler(requestContext)
   5:     }
   6: }
   7:  
   8: // The legacy HttpHandler that handles the request
   9: public class LegacyHandler : MvcHandler {
  10:     public LegacyHandler(RequestContext requestContext) : base(requestContext) { }
  11:  
  12:     protected override void ProcessRequest(HttpContextBase httpContext) {
  13:         string redirectActionName = ((LegacyRoute)RequestContext.RouteData.Route).RedirectActionName;
  14:  
  15:         // ... copy all of the querystring parameters and put them within RouteContext.RouteData.Values
  16:  
  17:         VirtualPathData data = RouteTable.Routes.GetVirtualPath(RouteContext, redirectActionName, RouteContext.RouteData.Values);
  18:  
  19:         httpContext.Status = "301 Moved Permanently";
  20:         httpContext.AppendHeader("Location", data.VirtualPath);
  21:     }
  22: }

Lastly, you need to create your routes within the Global.asax file. Remember, that order is necessary when setting up routing.

   1: public void RegisterRoutes(RouteCollection routes) {
   2:     routes.MapRoute("Login", "site/login", new {
   3:         controller = "Users",
   4:         action = "DisplayLogin"
   5:     });
   6:  
   7:     routes.Add("", new LegacyRoute(
   8:         "Users/Login.aspx",
   9:         "Login",
  10:         new LegacyRouteHandler()));
  11: }

And that's it. When a request comes in, you'll see the following in Fiddler

  1. A request on "Users/Login.aspx"
  2. A HTTP 301, with a header "Location" and value of "site/login"
  3. A request on "site/login"

Final Thoughts

Granted, there's more you can do with this - like creating your own extension methods like MapRoute and doing better handling of finding the route, but this should get you started. Also, I'm writing the code off the top of my head, so there's no guarantee that any of it works as-is. Please let me know if you have any other thoughts.

 

Lastly, for those wondering why are we using a HTTP 301 status code? Well read up on them. "301 Moved Permanently" indicates "that all future requests should be directed to the given URI." While your end users will not see any difference other than a new URL in the browser, the 301 status code more aimed towards search engines to update their URL's in their indexes.

 

Click here to download the source code for this example.

 

kick it on DotNetKicks.com

ASP.NET MVC - ActionResult... The Good & Not So Bad

April 18, 2008 18:21 by matthaw

I'm thoroughly enjoying the new ActionResult feature the ASP.NET team introduced into the "refresh" of the Preview 2 bits... or whatever they're calling it now :) Introducing this has increased productivity regarding testing the results of a controller's action. Here's a quick summary of the different types:

  • RenderViewResult - this result is returned when you call RenderView on the controller. It has properties like ViewName, MasterName, ViewEngine, ViewData, and TempData.
  • ActionRedirectResult - this result is returned when you call RedirectToAction on the controller. It exposes a dictionary of information.
  • HttpRedirectResult - this result is returned when you call Redirect. It exposes the Url that it is to redirect to.
  • EmptyResult - This is used for any other purpose in which you don't want the controller to complete the execution on. I have already used this for outputting HTML to the response stream for a specific purpose.

Now that these action results are present, there's no need to have your own view engine that captures the same information. What else is great, is that you can implement your own action result to do whatever you wish!

Onto the Not so Bad

1. When calling RenderView with the parameterless method, this renders testability useless because the RenderViewResult that is returned has Null values where you'd expect to find a value. For example, the critical property "ViewName" is null.

2. I feel that ActionRedirectResult should expose a ViewName and ViewData property. As it stands, when you call RedirectToAction, it takes your view name you've specified and puts it within the RouteValueDictionary it exposes. While this is still testable - it would be much easier to test if I could just go off of ViewName and ViewData.

All in all, this is a very good start to using ActionResults, and I love using them. Hope my insight helps!

kick it on DotNetKicks.com



New Drop of ASP.NET MVC

April 16, 2008 19:27 by matthaw

There's a new drop of ASP.NET MVC out on CodePlex now. Once again, there's some breaking changes, but nothing too terribly bad this time. Notably

  1. Action methods now return an ActionResult.
  2. It contains the changes to routing that Phil Haack mentioned.

Also, they've included all of the test cases too, so now you can be assured they are following good practices! Check out the Read Me for the latest changes in this drop.



MVC UI Validation Framework

April 2, 2008 16:36 by matthaw

The last few days I've been working on a MVC UI validation framework that closely follows that of ASP.NET. While my implementation is only in it's baby steps, I decided I would post it to the MVC Contrib project, and follow as much standards as what they have. Overall, the framework is very simple and straight forward in it's use.

Here were my requirements:

  1. It must closely match the ASP.NET UI validation framework.
  2. It should support all of the ASP.NET validators in their minimalistic implementation.
  3. It should require very minimal script inclusion or code to make this happen.
  4. It should only by a client side framework as you should be protecting your data on the server side through normal validations.

What I came up with, includes:

  1. It utilizes the ASP.NET UI validation framework by leveraging the WebUIValidation.js file (emitted through WebResource.axd).
  2. It supports all of the validators, with extended support for validation groups:
    1. RequiredValidator
    2. RegularExpressionValidator
    3. RangeValidator (currently does not support Date or Currency data types)
    4. CompareValidator
    5. CustomValidator
  3. It exposes 2 script inclusion calls, and 1 form validation setup call
    1. ValidatorRegistrationScripts() - Will render the WebUIValidation.js script, and scripts for form validation upon submission. This should be placed within the head tag.
    2. ValidatorInitializationScripts() - Will render all of the "expando" attributes for all validators upon the page and any initialization calls to set things up. This should be placed at the very end of your page before the closing body tag.
    3. FormValidation() - Will return an IDictionary object containing the "onsubmit" attribute. This should be called when creating your <form> tag.
  4. It is only a client-side framework leveraging ASP.NET's WebUIValidation.js

Since you now have an idea of the requirements and what was met, here's an example of it in action:

   1:  <%@ Import Namespace="MvcContrib.UI.Html" %>
   2:  <html>
   3:     <head>
   4:        <%= Html.Form().ValidatorRegistrationScripts() %>
   5:     </head>
   6:     <body>
   7:        <% using(Html.Form<MyController>(c => c.Save(), FormMethod.Post, Html.Form().FormValidation())) { %>
   8:        First Name: <%= Html.TextBox("firstName") %>
   9:        <%= Html.Form().RequiredValidator("firstNameRequired", "firstName", "First Name is Required.") %>
  10:        <%= Html.Form().RegularExpressionValidator("firstNameRegex", "firstName", "[a-zA-Z]*", "First Name can only contain letters.") %>
  11:        <br />
  12:        Age: <%= Html.TextBox("age") %>
  13:        <%= Html.Form().RequiredValidator("ageRequired", "age", "Age is Required.") %>
  14:        <%= Html.Form().RegularExpressionValidator("ageRegex", "age", "[0-9]*", "Age can only be numeric.") %>
  15:        <%= Html.Form().RangeValidator("ageRange", "age", "18", "35", ValidationDataType.Integer, "Age is not in target range of 18-35.") %>
  16:        <br />
  17:        <%= Html.SubmitButton("submit", "Save") %>
  18:        <% } %>
  19:        <%= Html.Form().ValidatorInitializationScripts() %>
  20:     </body>
  21:  </html>

At this point, I've posted it as a patch to MVC Contrib project, and hoping they apply it Smile. If you would like to get your hands on it, check out the patches page, mine is #1063. Please let me know your input, I tried making it as simple as possible to use, and I believe I've achieved that.

Update: My patch has been applied by Eric :) Get the latest build (>= 0.0.1.96) it if you'd like to check this out. Also, let me know of any issues you may find by logging bugs on the CodePlex site.





Copyright © 2000 - 2024 , Excentrics World