eWorld.UI - Matt Hawley

Ramblings of Matt

Using Razor Pages with WebForms Master Pages

January 7, 2011 14:56 by matthaw

There's been some recent chatter about mixing ASP.NET MVC with WebForms, which is something I'm all too familiar with. This setup works just fine, but what if you wanted to use the new Razor view engine within ASP.NET MVC? A bit of backstory.

 

On a secondary project that I work on within Microsoft, we were given the ability to rewrite the entire UI of the application in ASP.NET MVC. At that time, the first beta of MVC3 was rolling out and we decided to take our first plunge and use the Razor view engine. Unfortunately, the common UI layout that is shared across multiple properties that we had to use was WebForms based. Our attempts to convince the developers of the common UI components to support more of a modularized infrastructure failed for any foreseeable future, so we were left with the conundrum of figuring out an intermediary solution.

 

After discussions with the ASP.NET team to see if using a Razor view where it's layout was a WebForms master page was or could be a supported scenario - the general consensus being "No". The primary reason is that WebForms and Razor pages have a completely different architecture and runtime execution model (renders from a control tree vs. mostly-single pass template, respectively). However, what ASP.NET MVC does allow is context switching between view engines based on partials that are rendered. With that in mind, I decided to see if a partial based implementation could be achieved.

 

The solution is fairly simple, and provides an easy upgrade path if and when you could ditch the WebForms master page. We'll start by creating a few extensions to the controller for rendering Razor based views. The reason we're doing this, is so that a WebForms based view can be rendered, while you think you're rendering a Razor based view.

 

   1:  public static ViewResult RazorView(this Controller controller)
   2:  {
   3:      return RazorView(controller, null, null);
   4:  }
   5:   
   6:  public static ViewResult RazorView(this Controller controller, object model)
   7:  {
   8:      return RazorView(controller, null, model);
   9:  }
  10:   
  11:  public static ViewResult RazorView(this Controller controller, string viewName)
  12:  {
  13:      return RazorView(controller, viewName, null);
  14:  }
  15:   
  16:  public static ViewResult RazorView(this Controller controller, string viewName, object model)
  17:  {
  18:      if (model != null)
  19:          controller.ViewData.Model = model;
  20:   
  21:      controller.ViewBag._ViewName = GetViewName(controller, viewName);
  22:   
  23:      return new ViewResult
  24:      {
  25:          ViewName = "RazorView",
  26:          ViewData = controller.ViewData,
  27:          TempData = controller.TempData
  28:      };
  29:  }
  30:   
  31:  static string GetViewName(Controller controller, string viewName)
  32:  {
  33:      return !string.IsNullOrEmpty(viewName)
  34:          ? viewName
  35:          : controller.RouteData.GetRequiredString("action");
  36:  }

 

The thing to note from above, is that we're actually rendering a view called "RazorView" specifying the actual view to be rendered on ViewBag._ViewName. This value will later be used to render the appropriate RazorView.

Side note: This is a naive implementation of GetViewName assuming that if you do not specify the view name, you're rendering a view with the same action name from the route data. This may not be correct in all places, but seems to work reasonably well.

You'll also need to create a "RazorView" WebForms view in the Shared directory which inherits from your master page. This view will ultimately render the Razor view as a partial defined by your action result. I've also created a Razor layout page that all Razor views will use to alleviate the issue of having to touch each view if and when a conversion to only the Razor view engine takes place. You'll find each of the view files below.

 

Shared/Site.Master:

   1:  <%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
   2:  <!DOCTYPE html>
   3:  <html>
   4:  <head runat="server">
   5:      <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
   6:  </head>
   7:  <body>
   8:      <div>
   9:          <asp:ContentPlaceHolder ID="MainContent" runat="server" />
  10:      </div>
  11:  </body>
  12:  </html>

Shared/_SiteLayout.cshtml:

   1:  @RenderBody()

Shared/RazorView.aspx:

   1:  <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
   2:      Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
   3:   
   4:  <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
   5:      <% Html.RenderPartial((string) ViewBag._ViewName); %>
   6:  </asp:Content>

Home/Index.cshtml:

   1:  @{
   2:      Layout = "~/Views/Shared/_SiteLayout.cshtml";
   3:  }
   4:   
   5:  <h2>This is the index page</h2>

 

What you'll notice, is that it's not possible to utilize Razor content sections to render different parts from the WebForms master page. However, you are able to use Razor content sections from the Razor layout file since you're in that model. If you do have to render multiple WebForm content sections, you may need to make things a bit more elaborate by specifying specific content areas manually. Ultimately, I wouldn't recommend that, and just try to limit your output to a main content panel.

 

Within your controller, your action result is simply:

   1:  public ActionResult Index()
   2:  {
   3:      return this.RazorView();
   4:  }

 

While this solution isn't optimal, it does allow you to utilize Razor views while still adhering to a common WebForms master page. If possible, I recommend not doing this, but sometimes (like in our solution) you have to do what is necessary. If your master pages are simple enough (and primarily just layout), I fully recommend having a WebForms master page and a Razor layout page. Regardless, the way this has been setup, migration to a Razor only architecture is fairly trivial - just simply change all this.RazorView() calls to View() calls and copy over your layout to _SiteLayout.cshtml.

 

Click here to download the source for this solution.



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:      }
   8:   
   9:      protected override IView CreatePartialView(ControllerContext controllerContext, 
  10:                                                                            string partialPath)
  11:      {
  12:          return new LocalizationWebFormView(partialPath, null);
  13:      }
  14:  }
  15:   
  16:  public class LocalizationWebFormView : WebFormView
  17:  {
  18:      internal const string ViewPathKey = "__ViewPath__";
  19:   
  20:      public LocalizationWebFormView(string viewPath) : base(viewPath)
  21:      {
  22:      }
  23:   
  24:      public LocalizationWebFormView(string viewPath, string masterPath) 
  25:                : base(viewPath, masterPath)
  26:      {
  27:      }
  28:   
  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];
  34:          
  35:          viewContext.ViewData[ViewPathKey] = ViewPath;
  36:          base.Render(viewContext, writer);
  37:          
  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:      }
   9:   
  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 = "~/";
  16:   
  17:          ResourceExpressionFields fields = GetResourceFields(expression, path);
  18:          if (!string.IsNullOrEmpty(fields.ClassKey))
  19:              return GetGlobalResource(fields, args);
  20:   
  21:          return GetLocalResource(path, fields, args);
  22:      }
  23:   
  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:      }
  30:   
  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:      }
  36:   
  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:  }
   5:   
   6:  public class LocalizedViewPage<TModel> : ViewPage<TModel>, ILocalizedView
   7:      where TModel : class
   8:  {
   9:      public ResourceHelper Resource { get; set; }
  10:  }
  11:   
  12:  public class LocalizedViewUserControl : ViewUserControl, ILocalizedView
  13:  {
  14:      public ResourceHelper Resource { get; set; }
  15:  }
  16:   
  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:      }
   9:   
  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:      }
  16:   
  17:   
  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:  }
  27:   
  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.");
  36:   
  37:          return GetGlobalResource(fields, args);
  38:      }
  39:   
  40:      protected string GetGlobalResource(ResourceExpressionFields fields, object[] args)
  41:      {
  42:          return ResourceExtensions.GetGlobalResource(fields, args);
  43:      }
  44:   
  45:      protected ResourceExpressionFields GetResourceFields(string expression, string virtualPath)
  46:      {
  47:          return ResourceExtensions.GetResourceFields(expression, virtualPath);
  48:      }
  49:  }
  50:   
  51:  public class WebFormResourceHelper : ResourceHelper
  52:  {
  53:      public WebFormResourceHelper(string virtualPath)
  54:      {
  55:          VirtualPath = virtualPath;
  56:      }
  57:   
  58:      public string VirtualPath { get; private set; }
  59:   
  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);
  65:   
  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") %>
   3:   
   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;
   3:   
   4:  public class HomeController : Controller
   5:  {
   6:     public ActionResult Index()
   7:     {
   8:        return View();
   9:     }
  10:   
  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:      };
   8:   
   9:      Dictionary<object, object> data = 
  10:          MvcControlDataBinder.SourceToDictionary(products, "Id", "Name");
  11:      ViewData["Data"] = data;
  12:   
  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:      }
   9:   
  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";
   7:   
   8:          Product product = new Product(1, "16 Speed", 499.99m);
   9:   
  10:          ViewDataDictionary<Product> subViewData = 
  11:                    new ViewDataDictionary<Product>(product);
  12:          subViewData["ShippingCost"] = 24.99m;
  13:          ViewData.SubDataItems.Add("ProductData", subViewData);
  14:   
  15:          return View();
  16:      }
  17:  }
  18:   
  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;
  27:   
  28:          ViewData["OrderAmount"] = product.Price;
  29:          ViewData["Tax"] = tax;
  30:          ViewData["Total"] = total;
  31:   
  32:          ViewDataDictionary<Product> subViewData = 
  33:                     new ViewDataDictionary<Product>(product);
  34:          subViewData["ShippingCost"] = shipping;
  35:          ViewData.SubDataItems.Add("ProductData", subViewData);
  36:   
  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



Lambda Based RedirectToAction Sample Updated to MVC Preview 3

May 29, 2008 18:44 by matthaw

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

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

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

kick it on DotNetKicks.com



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

ViewData "dot" Notation Expressions in ASP.NET MVC

May 29, 2008 06:48 by matthaw

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

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

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

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

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

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

You would write the following syntax in your view:

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

Given that you have not set the following,

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

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

ViewData["FooObj"] = foo;

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

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

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

kick it on DotNetKicks.com



ASP.NET MVC - Localization Helpers

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

 

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

 

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

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

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

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

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

 

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

 

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

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

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

Here's the Confirm view:

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

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

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

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

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

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

English Spanish

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

kick it on DotNetKicks.com





Copyright © 2000 - 2024 , Excentrics World