eWorld.UI - Matt Hawley

Ramblings of Matt

ASP.NET MVC: Simplified Localization via ViewEngines

October 22, 2008 16:56 by matthaw

Thanks to Brad, he identified a few areas that made things MUCH simpler from my prior implementation. Notably, while you still have your ViewEngine specify "here's your view's path" there's no longer a need for having a new helper, derived classes, etc. This implementation goes back to the simplified HtmlHelper extension in which you can use in any ViewEngine regardless. It also allows you to "replace" the implementation by just changing the namespace, just as you can do with Html and Ajax.


Simplified View Engine

As previously stated, you still need a new view engine and view, because this is the only point in which you can truly grab which view is being rendered. What's nice now, is that it has been simplified down to using the base implementation and only setting a ViewData field in which the Resource extension method picks up and uses.

   1:  public class LocalizationWebFormViewEngine : WebFormViewEngine
   2:  {
   3:      protected override IView CreateView(ControllerContext controllerContext, 
   4:                                                  string viewPath, string masterPath)
   5:      {
   6:          return new LocalizationWebFormView(viewPath, masterPath);
   7:      }
   9:      protected override IView CreatePartialView(ControllerContext controllerContext, 
  10:                                                                            string partialPath)
  11:      {
  12:          return new LocalizationWebFormView(partialPath, null);
  13:      }
  14:  }
  16:  public class LocalizationWebFormView : WebFormView
  17:  {
  18:      internal const string ViewPathKey = "__ViewPath__";
  20:      public LocalizationWebFormView(string viewPath) : base(viewPath)
  21:      {
  22:      }
  24:      public LocalizationWebFormView(string viewPath, string masterPath) 
  25:                : base(viewPath, masterPath)
  26:      {
  27:      }
  29:      public override void Render(ViewContext viewContext, TextWriter writer)
  30:      {
  31:          // there seems to be a bug with RenderPartial tainting the page's view data
  32:          // so we should capture the current view path, and revert back after rendering
  33:          string originalViewPath = (string) viewContext.ViewData[ViewPathKey];
  35:          viewContext.ViewData[ViewPathKey] = ViewPath;
  36:          base.Render(viewContext, writer);
  38:          viewContext.ViewData[ViewPathKey] = originalViewPath;
  39:      }
  40:  }

As you can see, it's much simpler now. Also, it seems as if there's a bug with the current Beta bits when you have a RenderPartial on your page, there's no isolation and the view's path is not correct later. Simple fix by simply grabbing the original view path, setting it to the new path, rendering, and then reverting back.


Getting back to Extension Methods

And now that we have our path again in our ViewData, we can revert back to using extension methods to extract the path when doing both Global and Local resources. As you can see below, should you be using any other ViewEngine it will always work for Global resources.

   1:  public static class ResourceExtensions
   2:  {
   3:      public static string Resource(this Controller controller, string expression, 
   4:                                                    params object[] args)
   5:      {
   6:          ResourceExpressionFields fields = GetResourceFields(expression, "~/");
   7:          return GetGlobalResource(fields, args);
   8:      }
  10:      public static string Resource(this HtmlHelper htmlHelper, 
  11:                                                    string expression, params object[] args)
  12:      {
  13:          string path = (string)htmlHelper.ViewData[LocalizationWebFormView.ViewPathKey];
  14:          if (string.IsNullOrEmpty(path))
  15:              path = "~/";
  17:          ResourceExpressionFields fields = GetResourceFields(expression, path);
  18:          if (!string.IsNullOrEmpty(fields.ClassKey))
  19:              return GetGlobalResource(fields, args);
  21:          return GetLocalResource(path, fields, args);
  22:      }
  24:      static string GetLocalResource(string path, ResourceExpressionFields fields, 
  25:                                                       object[] args)
  26:      {
  27:          return string.Format((string)HttpContext.GetLocalResourceObject(path, 
  28:                                 fields.ResourceKey, CultureInfo.CurrentUICulture), args);
  29:      }
  31:      static string GetGlobalResource(ResourceExpressionFields fields, object[] args)
  32:      {
  33:          return string.Format((string)HttpContext.GetGlobalResourceObject(
  34:               fields.ClassKey, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
  35:      }
  37:      static ResourceExpressionFields GetResourceFields(string expression, 
  38:                                                                string virtualPath)
  39:      {
  40:          var context = new ExpressionBuilderContext(virtualPath);
  41:          var builder = new ResourceExpressionBuilder();
  42:          return (ResourceExpressionFields)builder.ParseExpression(
  43:                                            expression, typeof(string), context);
  44:      }
  45:  }

Again, thanks to Brad, this implementation seems "less dirty". You can download the source for this here.


kick it on DotNetKicks.com

ASP.NET MVC: Localization Delegated via View Engines

October 22, 2008 14:02 by matthaw

Update: Please see this newer post. It simplifies this solution drastically, and I don't recommend this version anymore!



I've previously blogged about how to add localization support within ASP.NET MVC before, but that codebase was based on Preview 3, and now that the beta is out there, the code just doesn't work anymore. Period. The concepts I had originally had put out there really don't fly well in the Beta world, or, within the "real" world in which people are using more than just the WebFormViewEngine for their rendering. So, why is there this big disconnect now? Well. it's not so much that global resources fail to work, it's more local resources don't really apply "generally" anymore.


While looking into a solution, and bouncing ideas off of Phil & Brad, I came to the realization that, just like in the Matrix, "There is no spoon" or rather, replace "spoon" with "view location" and you have your reasoning. Now, before you start ranting, let me explain. Yes, there are physical locations at which your views are located, but what if you're ViewEngine was pulling the view's from a database? No guarantee that a "general" solution could achieve local resource support based on a HtmlHelper extensino. However, if you left the local resource delegation up to a ViewEngine, you can now have a concept of a location in relevance to it. (And yes, the point may be moot if you use a different Resource Provider than the asp.net built-in one)


So where does that leave us? Well, we need a general way to access both global and local resources within a View, and global resources within a Controller. Ultimately, the code used to get the resources hasn't changed from my original implementation, however, because we no longer have that concept of a view location, the new implementation delegates this decision correctly to the view engine. Also, before I show the proof of concept, I want to warn you that the guts for my view rendering logic is nearly a copy from what is in the WebFormView source.


So here's the overall design is as follows:

  1. Any view engine / views can out-of-the-box use "global resources"
  2. A view engine can decide if it's views can participate in "local resources" by supplying it's own implementation.
  3. A controller can request "global resources" for messages it's setting within ViewData.

Now, because I wanted to provide a default implementation for the WebFormViewEngine, the implementation isn't nearly as clean as one would like, but it does work great, and falls back to using non-derived ViewPage's and ViewUserControl's when necessary.


View Implementation

We start by deriving ViewPage and ViewUserControl to support localization. These new implementations will add a ResourceHelper, just like Ajax / Html / Url, so that within your view, you can directly access a resource.

   1:  public class LocalizedViewPage : ViewPage, ILocalizedView
   2:  {
   3:      public ResourceHelper Resource { get; set; }
   4:  }
   6:  public class LocalizedViewPage<TModel> : ViewPage<TModel>, ILocalizedView
   7:      where TModel : class
   8:  {
   9:      public ResourceHelper Resource { get; set; }
  10:  }
  12:  public class LocalizedViewUserControl : ViewUserControl, ILocalizedView
  13:  {
  14:      public ResourceHelper Resource { get; set; }
  15:  }
  17:  public class LocalizedViewUserControl<TModel> : ViewUserControl<TModel>, ILocalizedView
  18:      where TModel : class
  19:  {
  20:      public ResourceHelper Resource { get; set; }
  21:  }

The interface ILocalizedView is simply an abstraction I put out so I could determine what type of View I was working with in the View Engine.


Resource Helper Implementation

The resource helper implementation is very straight forward. As previously stated, ResourceHelper is the base that adds support for global resources. The WebFormResourceHelper is what the new web form view engine implementation uses to add support for both global and local resources. As you'll see, WebFormResourceHelper is initialized with the virtual path of the currently rendering view so that it can correctly find the local resource.

   1:  public static class ResourceExtensions
   2:  {
   3:      public static string Resource(this Controller controller, 
   4:                                         string expression, params object[] args)
   5:      {
   6:          ResourceExpressionFields fields = GetResourceFields(expression, "~/");
   7:          return GetGlobalResource(fields, args);
   8:      }
  10:      internal static string GetGlobalResource(ResourceExpressionFields fields, object[] args)
  11:      {
  12:          return string.Format((string)HttpContext.GetGlobalResourceObject(
  13:                                                      fields.ClassKey, fields.ResourceKey, 
  14:                                                      CultureInfo.CurrentUICulture), args);
  15:      }
  18:      internal static ResourceExpressionFields GetResourceFields(
  19:                                    string expression, string virtualPath)
  20:      {
  21:          var context = new ExpressionBuilderContext(virtualPath);
  22:          var builder = new ResourceExpressionBuilder();
  23:          return (ResourceExpressionFields)builder.ParseExpression(
  24:                                    expression, typeof(string), context);
  25:      }
  26:  }
  28:  public class ResourceHelper
  29:  {
  30:      public virtual string GetString(string expression, params object[] args)
  31:      {
  32:          ResourceExpressionFields fields = GetResourceFields(expression, "~/");
  33:          if (string.IsNullOrEmpty(fields.ClassKey))
  34:              throw new InvalidOperationException(
  35:                       "The resource helper does not support local resources.");
  37:          return GetGlobalResource(fields, args);
  38:      }
  40:      protected string GetGlobalResource(ResourceExpressionFields fields, object[] args)
  41:      {
  42:          return ResourceExtensions.GetGlobalResource(fields, args);
  43:      }
  45:      protected ResourceExpressionFields GetResourceFields(string expression, string virtualPath)
  46:      {
  47:          return ResourceExtensions.GetResourceFields(expression, virtualPath);
  48:      }
  49:  }
  51:  public class WebFormResourceHelper : ResourceHelper
  52:  {
  53:      public WebFormResourceHelper(string virtualPath)
  54:      {
  55:          VirtualPath = virtualPath;
  56:      }
  58:      public string VirtualPath { get; private set; }
  60:      public override string GetString(string expression, params object[] args)
  61:      {
  62:          ResourceExpressionFields fields = GetResourceFields(expression, VirtualPath);
  63:          if (!string.IsNullOrEmpty(fields.ClassKey))
  64:              return GetGlobalResource(fields, args);
  66:          return string.Format((string) HttpContext.GetLocalResourceObject(
  67:                                                             VirtualPath, fields.ResourceKey, 
  68:                                                             CultureInfo.CurrentUICulture), args);
  69:      }
  70:  }

You'll also notice that I've included a controller extension method for extracting global resources. This hasn't changed from the original implementation, but it's straight forward to use.


View Engine Implementation

This is where the rubber meets the road. Since we're now delegating that a ViewEngine can choose to support local resources, the derived view engine implementation is where it supplies it's own ResourceHelper, if it so chooses. In my example, I derive from the WebFormViewEngine and WebFormView, and provide this implementation to supply the WebFormResourceHelper to my localized views. For brevity, and the fact that I didn't write the code for rendering, I'll leave it up to you to see the full implementation for the LocalizedWebFormView in the download. However, the critical portion that you'll see is the following:

   1:  var localizedPage = page as ILocalizedView;
   2:  if (localizedPage != null)
   3:      localizedPage.Resource = new WebFormResourceHelper(ViewPath);
   4:  page.RenderView(context);

And that's it. In your view, you can now derive from LocalizedViewPage and get access to your global and local resources by doing

   1:  // global resource
   2:  <%= Resource.GetString("MvcExample, Welcome") %>
   4:  // local resource
   5:  <%= Resource.GetString("Welcome") %>


How does this fit with other View Engines?

View engines need to simply add a reference to the ResourceHelper so that their view's can gain access to it. Obviously, they'll also need to change their ViewEngine to set an appropriate ResourceHelper when instantiating the view, but those view engines can explicitly determine how they would like to treat resources (even completely ignoring Global & Local resources).


And that's it, our View Engine now is completely in control of where my resources come from. They can use the baked-in ASP.NET resource provider, their own logic, or whatever! Again, this implementation is no where near baked, and I strongly urge you to look at your solution to see how this would fit in, and (obviously) make the code "production worthy". I'm hoping that some of these concepts make it into future MVC releases. You can also download the source for these files here. Enjoy!


kick it on DotNetKicks.com

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

ASP.NET MVC Preview 4 New Finds

July 17, 2008 00:49 by matthaw

So I've been checking over the source code for the latest ASP.NET MVC P4 release out on CodePlex, and have found some very interesting things.

 1. There's a variant codebase that is very similar to my RedirectToAction via Lambda expressions contained within the Futures/MvcFutures/Internal directory, "ExpressionHelper.cs". With this futures drop, the call will return a RouteValueDictionary, in which you turn around and call RedirectToRoute. In my version, of RedirectToAction, this is all taken care of for you, as well as it supports not having to explicitly give the type of your controller. Either way, here's the example on how to use this if you prefer:

   1:  using Microsoft.Web.Mvc.Internal;
   2:  using System.Web.Routing;
   4:  public class HomeController : Controller
   5:  {
   6:     public ActionResult Index()
   7:     {
   8:        return View();
   9:     }
  11:     public ActionResult DoRedirect()
  12:     {
  13:        RouteValueDictionary values = 
  14:           ExpressionHelper.GetRouteValuesFromExpression<HomeController>(c => c.Index());
  15:        return RedirectToRoute(values);
  16:     }
  17:  }

2. The RedirectToAction bug I blogged about has been fixed.

3. They've introduced a helper extension method (MvcControlDataBinder.SourceToDictionary) to convert data sources to a dictionary of values. The supported data source types include

  1. DataSet
  2. DataTable
  3. IDataReader
  4. IList
  5. Array
  6. IQueryable
  7. IEnumerable

Below is an example on how to use this method. While I would never use this to convert a List of models normally, I could see where if you simply need to get truncated key/value list of data with various other sources like IDataReader or IQueryable, this would be beneficial.

   1:  public ActionResult Products()
   2:  {
   3:      List<Product> products = new List<Product>()
   4:      {
   5:          new Product(1, "16 Speed", 499.99m),
   6:          new Product(2, "24 Speed", 699.99m)
   7:      };
   9:      Dictionary<object, object> data = 
  10:          MvcControlDataBinder.SourceToDictionary(products, "Id", "Name");
  11:      ViewData["Data"] = data;
  13:      return View();
  14:  }

4. The new Ajax support. I'm not going to detail this as Scott Hanselman's post covered a good example and ScottGu will eventually give Part 2 which will be very detailed.

5. As ScottGu already indicated, the Mvc Framework comes with a bunch of new Action Filters, including

  1. AuthorizeAttribute - which uses the membership framework for user authentication and user & role based security.
  2. HandleErrorAttribute - which will enable you to handle exceptions that occur by rendering a specific view when an error occurs. This is equitable to the catch-all error page.
  3. OutputCacheAttribute - Yes, this is what it says it is. Think ASP.NET output caching, and thats what this is.

6. TempData is now "testing friendly"! Well, right out of the box now, that is. They've introduced a new interface ITempDataProvider (and subsequent SessionStateTempDataProvider) that will only be used only when Execute is called from the Controller. The reason TempData is now tester friendly, is that by default it uses TempDataDictionary as a backing field, and Execute will Load/Save from the TempDataProvider set to/from TempData.

kick it on DotNetKicks.com

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

MVC Post-Redirect-Get Sample Updated

June 5, 2008 08:56 by matthaw

I took some time to look back at my MVC Post-Redirect-Get sample and see where it could be improved as well as update it to use the MVC Preview 3 bits. What I found, is again, the core concepts didn't change that much. However, there are some new enhancements that Preview 3 gave us that makes our life a little bit easier. I'll save the full implementation for you to download and checkout yourself, but I do want to highlight some of these enhancements that made the source easier to use.

1. NameValueCollectionExtensions.CopyTo - this made it very nice for me to take all of the posted form data and copy it into the TempData so that upon a redirect, I could extract it out and put it in ViewData.

   1:  if (!BaseValidator.Validate(HttpContext.Request, validators)) {
   2:     NameValueCollectionExtensions.CopyTo(Request.Form, TempData);
   3:     TempData["ErrorMessage"] = BuildErrorMessage(validators);
   4:     return RedirectToAction("Create");
   5:  }

2. I added an extension method for IDictionary to copy between a source and destination, primarily for copying my TempData to ViewData. This way there is no need to do a manual copy of TempData objects all over the place, and is more resilient to changes and additions within your views and controller actions. I'm hoping this makes it into the MVC stack at some point so we all don't have to write this code ourselves.

   1:  public static void CopyTo(this IDictionary<string, object> source, 
   2:                            IDictionary<string, object> destination)
   3:  {
   4:      foreach (KeyValuePair<string, object> pair in source)
   5:      {
   6:          if (!destination.ContainsKey(pair.Key))
   7:              destination.Add(pair.Key, pair.Value);
   8:      }
   9:  }

The above code allows us to simply call the following line of code

   1:  TempData.CopyTo(ViewData);

3. What you'll notice, is that now all of my data is within ViewData, I can start to utilize the built-in functionality added in Preview 3 where the form controls will attempt to extract an initial value from ViewData. This mechanism really brings back the concept of ViewState, except that there is really no overhead to do this! Here's how my form now looks

   1:  <%@ Import Namespace="PRG.Controllers" %>
   2:  <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
   3:  <% using (Html.Form<ProductsController>(c => c.Submit())) { %>
   4:      <h3>Create a New Product</h3>
   5:      Name: <%= Html.TextBox("Name") %><br />
   6:      Price: <%= Html.TextBox("Price") %><br />
   7:      Quantity: <%= Html.TextBox("Quantity") %><br />
   8:      <% if (ViewData["ErrorMessage"] != null) { %>
   9:          <br /><span style="color:red"><%= ViewData["ErrorMessage"] %></span><br />
  10:      <% } %>
  11:      <br />
  12:      <%= Html.SubmitButton() %>
  13:  <% } %>
  14:  </asp:Content>

4. While this next item isn't specifically related to Preview 3, it is a change from my last sample. Previously, I was doing all of my manual validations inline on the server, and it wasn't pretty. As you've probably been reading, I've been making some improvements to the MVC Validation within MvcContrib, and I decided I'd bring in that codebase to this sample. However, to truly show the full PRG pattern, I needed my form to post and alert me that there are errors on the page rather than relying upon client side validation; so I'm simply using the validator objects & server validation. In the next coming weeks, I'll be making another update to MvcContrib to do model based validation. I'll leave this code for your viewing or other examples on my blog.

And that's the updated sample. You can downloaded the latest bits from here. Please let me know what you think and anything else you would do to change this. As each iteration of the MVC framework is released, the sample gets easier and easier! Hope you enjoy this.

kick it on DotNetKicks.com

Using SubDataItems and View User Controls in ASP.NET MVC

June 4, 2008 08:46 by matthaw

Prior to the preview 3 release of ASP.NET MVC, whenever you wanted to pass data to your view user controls, you only had the option of passing a specific object, or using it's parent's view data. This was all great, and it worked wonderfully, but the problem existed that your view user control would always be dependant upon some parent view data. ASP.NET MVC preview 3 introduced the concept of SubDataItems off of ViewDataDictionary in which you could specify a keyed ViewDataDictionary to use. This helps in the true separation of your view user controls, or child views, and will hopefully later lead into more interesting solutions such as sub-controllers. So lets get into an example.

Hold It! Yeah, we have to patch the MVC framework first before doing anything further. I've logged a bug with the ASP.NET MVC team regarding this, and it's unfortunate to say that this shouldn't have slipped through - but that's what you get without test cases. But I digress. Okay, open the MVC Preview 3 source and open the file "System.Web.Mvc/Mvc/Extensions/UserControlExtensions.cs". Go to the "DoRendering" method, and replace the if block starting on line 127 with the following:

   1:  if (controlData != null) {
   2:      instance.ViewData.Model = controlData;
   3:  }
   4:  else if (!string.IsNullOrEmpty(instance.SubDataKey)) {
   5:      instance.ViewData = context.ViewData.SubDataItems[instance.SubDataKey];
   6:  }
   7:  else {
   8:      instance.ViewData = context.ViewData;
   9:  }


Okay, onto the example. Say you have a product detail on your website, and you need to display the product information on the listing page as well as on the order summary page. Because we want to display the same information in the same way on each page, it leads us into using a ViewUserControl. There's other information listed on each page itself, so there is definitely no need to create or duplicate our view data model object just for display purposes. First, we should define our model.

   1:  public class Product
   2:  {
   3:      public Product(int id, string name, decimal price)
   4:      {
   5:          Id = id;
   6:          Name = name;
   7:          Price = price;
   8:      }
  10:      public int Id { get; set; }
  11:      public string Name { get; set; }
  12:      public decimal Price { get; set; }
  13:  }

Next, let's work on our controllers. To show the true separation, I'll have both a ProductController and OrderController.

   1:  public class ProductController : Controller
   2:  {
   3:      public ActionResult Index()
   4:      {
   5:          ViewData["Category"] = "Bicycles";
   6:          ViewData["SubCategory"] = "Mountain Bikes";
   8:          Product product = new Product(1, "16 Speed", 499.99m);
  10:          ViewDataDictionary<Product> subViewData = 
  11:                    new ViewDataDictionary<Product>(product);
  12:          subViewData["ShippingCost"] = 24.99m;
  13:          ViewData.SubDataItems.Add("ProductData", subViewData);
  15:          return View();
  16:      }
  17:  }
  19:  public class OrderController : Controller
  20:  {
  21:      public ActionResult Index()
  22:      {
  23:          Product product = new Product(1, "16 Speed", 499.99m);
  24:          decimal shipping = 24.99m;
  25:          decimal tax = (product.Price * .08m);
  26:          decimal total = product.Price + shipping + tax;
  28:          ViewData["OrderAmount"] = product.Price;
  29:          ViewData["Tax"] = tax;
  30:          ViewData["Total"] = total;
  32:          ViewDataDictionary<Product> subViewData = 
  33:                     new ViewDataDictionary<Product>(product);
  34:          subViewData["ShippingCost"] = shipping;
  35:          ViewData.SubDataItems.Add("ProductData", subViewData);
  37:          return View();
  38:      }
  39:  }

As you can see, within both of the controller actions, I'm creating a new instance of a ViewDataDictionary<Product> and adding it to the ViewData.SubDataItems. This will allow me to later extract that specific ViewDataDictionary when rendering the ViewUserControl. Granted, this scenario is very rudimentary - but I wanted to show that each action / view has it's own data, but wanted to show the "sharing" of the SubDataItem data. Now, we implement our ViewUserControl (I won't show the HTML for this, it's in the download though). The ViewUserControl simply needs the following

   1:  public partial class ProductInfo 
   2:          : System.Web.Mvc.ViewUserControl<Models.Product> { }

Now, in both our Index views for product & order  we can call the helper method to render our view user control. Please note that within this, we're not specifying any control data (model) but we are specifying the SubDataKey. As you've probably figured out, that SubDataKey is the key the framework uses to extract the correct ViewDataDictionary to set on the ViewUserControl when rendering. Should you not specify a SubDataKey, it'll use the parent ViewDataDictionary - so in this case, our ViewPage's ViewDataDictionary.

   1:  <%= this.RenderUserControl("~/views/shared/ProductInfo.ascx", 
   2:                             null, 
   3:                             new { SubDataKey = "ProductData" }) %>

And that's it. Of course, SubDataItems can be used for other purposes like keeping your ViewData "componentized", but the best use for this so far is so that your ViewUserControl's can live without the knowledge or sharing of parent ViewPage's or ViewUserControl's. If you'd like the source for this demo, you can download it here. It already has the patched MVC assembly. Enjoy!

kick it on DotNetKicks.com

RedirectToAction Nasty Bug in ASP.NET MVC Preview 3

June 2, 2008 22:04 by matthaw

We were converting the CodePlex application over to ASP.NET MVC Preview 3 today and found a nasty bug with RedirectToAction. In reality, the bug isn't so much around RedirectToAction, but a change they made internally to Routing. However, this seems to only happen in certain routing scenarios. Take the following routes:

   1:  routes.MapRoute("Login", "site/home/{action}",
   2:     new { controller = "session", action = "login" });
   3:  routes.MapRoute("User Info", "site/user/{action}",
   4:     new { controller = "user", action = "show" });

The problem crops up within your UserController actions when attempting to redirect to other UserController actions. For instance

   1:  public class UserController : Controller {
   2:     public ActionResult Show() { ... }
   3:     public ActionResult Create() {
   4:        return RedirectToAction("Show");
   5:     }
   6:  }

when line 4 is executed above, it attempts to redirect you to "~/site/home/foo". The reason is the change that was made is now trying to remove all the ambiguities by "assuming" things on the fly. Since the actionName overload of RedirectToAction doesn't take and doesn't supply the controller it cannot find the appropriate route. After a bit of convincing, the bug has been acknowledge but I make no guarantees if and when it'll be fixed in a future build - but in the mean time, you're stuck with

  1. Fixing the code yourself from the CodePlex source drop. This simply involves supplying the current executing controller's name in the route value dictionary.
  2. Use the RedirectToAction overload that takes both actionName and controllerName.
  3. Use my lambda expression based RedirectToAction which correctly sends both actionName and controllerName.

And, before anyone asks - why are you creating such complicated routing? Well, simple - simple routing equals a very simple application. Complex routing equals a pre-existing and well defined "RESTful based" urls that mean something when a user reads it.

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:      }
  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");
  22:          decimal price;
  23:          if (!decimal.TryParse(Request.Form["Price"], out price))
  24:              error += this.Resource("Strings, PriceIsEmpty");
  26:          int quantity;
  27:          if (!int.TryParse(Request.Form["Quantity"], out quantity))
  28:              error += this.Resource("Strings, QuantityIsEmpty");
  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:          }
  39:          return RedirectToAction("Confirm");
  40:      }
  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")
   4:  // global resource with optional arguments for formatting
   5:  Html.Resource("GlobalResource, ResourceName", "foo", "bar")
   7:  // default local resource
   8:  Html.Resource("ResourceName")
  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:  }
   7:  public static string Resource(this Controller controller, string expression, params object[] args)
   8:  {
   9:    return GetResourceString(controller.HttpContext, expression, "~/", args);
  10:  }
  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);
  18:    if (!string.IsNullOrEmpty(fields.ClassKey))
  19:      return string.Format((string)httpContext.GetGlobalResourceObject(fields.ClassKey, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
  21:    return string.Format((string)httpContext.GetLocalResourceObject(virtualPath, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
  22:  }
  24:  private static string GetVirtualPath(HtmlHelper htmlhelper)
  25:  {
  26:    WebFormView view = htmlhelper.ViewContext.View as WebFormView;
  28:    if (view != null)
  29:      return view.ViewPath;
  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" }));
   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;
   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:      }
  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:      }
  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.");
  28:          if (body.Object != action.Parameters[0])
  29:              throw new InvalidOperationException("Method call must target lambda argument.");
  31:          string actionName = body.Method.Name;
  32:          string controllerName = typeof(T).Name;
  34:          if (controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
  35:              controllerName = controllerName.Remove(controllerName.Length - 10, 10);
  37:          RouteValueDictionary parameters = LinkBuilder.BuildParameterValuesFromExpression(body);
  39:          values = values ?? new RouteValueDictionary();
  40:          values.Add("controller", controllerName);
  41:          values.Add("action", actionName);
  43:          if (parameters != null)
  44:          {
  45:              foreach (KeyValuePair<string, object> parameter in parameters)
  46:              {
  47:                  values.Add(parameter.Key, parameter.Value);
  48:              }
  49:          }
  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;
   3:  public class UserController : Controller
   4:  {
   5:     public ActionResult Index()
   6:     {
   7:        return RenderView();
   8:     }
  10:     public ActionResult Add()
  11:     {
  12:        return RenderView(new UserData());
  13:     }
  15:     public ActionResult Save()
  16:     {
  17:        UserData data = new UserData();
  18:        BindingHelperExtensions.UpdateFrom(data, Request.Form);
  20:        if (!BaseValidator.Validate(Request.Form, data.Validators))
  21:           return RenderView("Add", data); // You should follow PRG pattern
  23:        // save the data
  25:        return RedirectToAction("Index");
  26:     }
  27:  }
  29:  public class ValidatorViewData
  30:  {
  31:     private IList<IValidator> validators = new List<IValidator>();
  33:     public IList<IValidator> Validators
  34:     {
  35:        get { return validators; }
  36:     }
  37:  }
  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:     }
  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 - 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.


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.



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(); " }) %>
   9:  <script type="text/javascript">
  10:  function mvcFormSubmit() {
  11:     $get('mvcFormSubmitted').value = 'true';
  12:  }
  13:  </script>


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" });
   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:     }
  12:     public ActionResult ProcessLogin()
  13:     {
  14:        if (Request.Form["mvcFormSubmitted"] != "true")
  15:           return RenderView("Login");
  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:        }
  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

Copyright © 2000 - 2024 , Excentrics World