Posterous theme by Cory Watilo

Filed under: ASP.NET

Adding MvcContrib SubControllers to your ASP.NET MVC Project

After a little bit of work with MVC, you get infected with the spirit of clean code and begin to desire even more ways of eliminating repetition. You’ve got partials and html helpers. Still you are hungry. SubControllers are the dish that will fill you up.

Why SubControllers?

There’s a design decision here. The question is why are we designing with subcontrollers? To understand the rationale, let’s look at the qualities of the various sub-view options.

  • Partial Views – partial views are probably the most useful method for re-using view output. They are very easy to set up, and are very versatile. However, they don’t contain logic. They are intended entirely as a slave to the Action Controller. They get their model from the controller and simply render it. This means that the controller needs to know and provide everything the partial view needs. Not good for something like a login status control, which is an separate concern from most controllers.
  • HTML Helpers – HTML helpers get used a lot, and they are very helpful for creating your own ‘controls’ to use in your pages. They do not, however, support using a view template. This means you’ve got to create them and test them with tests for emitted markup. This adds a lot of complexity if you are trying to do something more than create a html rendering function. Not a good place to insert complex view logic, like a shopping cart status widget, or anything with a table or list. The HTML helper also still gets it’s data from the master view/controller, so it fails at separating concerns.
  • Html.RenderAction() – RenderAction is the once and future solution to a number of problems. Or so I hear. I have trouble getting excited about using it right now. Currently, it is not baked into MVC, but rather is in the separate ‘MVC Futures’ assembly, where it is unsupported. Rumor is it is buggy and unsecure. In the future it may be changed or dropped or renamed or anything. ScottGu has promised in a blog comment that it is due for inclusion in MVC 2. However, I don’t program to unreleased Microsoft products. Maybe later, but for now…
  • MvcContrib SubController – Jeff Palermo implemented subcontrollers for MvcContrib to give us a working solution—now—for reusable view/controller code. Subcontrollers have their own model, ViewData, are nestable, and can use view templates for their output. If you have a job that goes beyond a partial view, or a html helper, SubControllers are a peach.

Setting Up Your Project

1) Add a reference to MvcContrib.

2) Create a class called StructureMapSubControllerBinder. This is only required if you’re using StructureMap to do your IOC for you. You can use the base SubControllerBinder from MvcContrib, or create your own version for your IOC tool.

public class StructureMapSubControllerBinder
: SubControllerBinder
{
public override object CreateSubController(Type
destinationType)
{
object instance = ObjectFactory.GetInstance(destinationType);
if (instance == null)
{
throw new InvalidOperationException(destinationType
+ " not registered with StructureMap");
}

return instance;
}
}

 

3) Make the SubControllerBinder your default binder.

ModelBinders.Binders.DefaultBinder = new StructureMapSubControllerBinder();
4) Add a reference to the MvcContrib namespace to the Pages section of Web.Config. This will save you putting a lot of namespace tags in your views.
<add namespace="MvcContrib"/>

Creating a SubController

1) Create a ~/Controllers/SubControllers folder. This is completely optional. If you have a bigger project you might want to make multiple subcontrollers folders for different areas.

2) Create a SubController class. The action needs to have the ‘same’ name as the class. It also subclasses from SubController.

using System.Web.Mvc;
using MvcContrib;

namespace NWIS.Business.Web.Controllers.SubControllers
{
public class DemoSubController
: SubController
{
public ViewResult Demo()
{
return View();
}
}
}

3) Create a View subfolder. Using the ‘Add View’ context menu from the controller won’t work because the ‘sub’ in the class name. Just create the folder yourself. ~/Views/Demo.

4) Create a View. Right click on the ~/Views/Demo folder. Select Add->View. Name the view ‘Demo’. Make it a partial view (.ascx). Go ahead an add some markup to the view. Whatever you like.

Using the SubController in your View

1) Add an attribute to your Controller class.

[SubControllerActionToViewDataAttribute]

2) Add the subcontroller as a parameter to the action you want to use it in.

public ViewResult
Index(DemoSubController mySubCont)

3) Place the subcontroller output into your view.

<% ViewData.Get<Action>("mySubCont").Invoke();
%>
The name you use here is the name of the parameter to the controller action.

That should be it. You subcontroller can output into your view, and you can use it in many, many controllers and actions. You can add dependencies directly to the subcontroller in it’s constructor. You can also pass information from the action to the subcontroller using properties.

Resources

Adding MVC to an existing ASP.NET Application

ASP.NET MVC Has added a very useful new programming model to developing websites in .NET. There is hype and debate all throughout the interwebs on how great MVC is. And it all can be yours, with nothing but a ‘File->New’ in Visual Studio….unless you already have an ASP.NET application. In which case, you need to do a bit more work.

  1. Add project references to:
    System.Web.Abstractions
    System.Web.Mvc
    System.Web.Routing
  2. Create /Controllers, /Views, and /Views/Shared folders in your project
  3. Update web.config
    <?xml version="1.0" encoding="utf-8" ?>
    
    <configuration>
    
    <system.web>
    
    <pages>
    
    <namespaces>
    
    <add namespace="System.Web.Mvc"/>
    
    <add namespace="System.Web.Mvc.Ajax"/>
    
    <add namespace="System.Web.Mvc.Html" />
    
    <add namespace="System.Web.Routing"/>
    
    <add namespace="System.Linq"/>
    
    <add namespace="System.Collections.Generic"/>
    
    </namespaces>
    
    </pages>
    
    <compilation>
    
    <assemblies>
    
    <add assembly="System.Core,
    Version=3.5.0.0,
    Culture=neutral,
    PublicKeyToken=B77A5C561934E089"/>
    
    <add assembly="System.Web.Mvc,
    Version=1.0.0.0,
    Culture=neutral,
    PublicKeyToken=31bf3856ad364e35" />
    
    <add assembly="System.Web.Abstractions,
    Version=3.5.0.0,
    Culture=neutral,
    PublicKeyToken=31BF3856AD364E35"/>
    
    <add assembly="System.Web.Routing,
    Version=3.5.0.0,
    Culture=neutral,
    PublicKeyToken=31BF3856AD364E35"/>
    
    </assemblies>
    
    </compilation>
    
    <httpModules>
    
    <add name="UrlRoutingModule"
    
    type="System.Web.Routing.UrlRoutingModule,
    System.Web.Routing,
    Version=3.5.0.0,
    Culture=neutral,
    PublicKeyToken=31BF3856AD364E35" />
    
    </httpModules>
    
    </system.web>
    
    <system.webServer>
    
    <validation validateIntegratedModeConfiguration="false"/>
    
    <modules runAllManagedModulesForAllRequests="true">
    
    <remove name="ScriptModule" />
    
    <remove name="UrlRoutingModule" />
    
    <add name="ScriptModule" preCondition="managedHandler"
    
    type="System.Web.Handlers.ScriptModule, 
    
    System.Web.Extensions, Version=3.5.0.0, Culture=neutral, 
    
    PublicKeyToken=31BF3856AD364E35"/>
    
    <add name="UrlRoutingModule"
    
    type="System.Web.Routing.UrlRoutingModule,
    System.Web.Routing, 
    
    Version=3.5.0.0, Culture=neutral, 
    
    PublicKeyToken=31BF3856AD364E35" />
    
    </modules>
    
    <handlers>
    
    <remove name="WebServiceHandlerFactory-Integrated"/>
    
    <remove name="ScriptHandlerFactory" />
    
    <remove name="ScriptHandlerFactoryAppServices" />
    
    <remove name="ScriptResource" />
    
    <remove name="MvcHttpHandler" />
    
    <remove name="UrlRoutingHandler" />
    
    <add name="ScriptHandlerFactory" verb="*" path="*.asmx"
    
    preCondition="integratedMode"
    
    type="System.Web.Script.Services.ScriptHandlerFactory, 
    
    System.Web.Extensions, Version=3.5.0.0, Culture=neutral, 
    
    PublicKeyToken=31BF3856AD364E35"/>
    
    <add name="ScriptHandlerFactoryAppServices" verb="*"
    
    path="*_AppService.axd" preCondition="integratedMode"
    
    type="System.Web.Script.Services.ScriptHandlerFactory, 
    
    System.Web.Extensions, Version=3.5.0.0, Culture=neutral, 
    
    PublicKeyToken=31BF3856AD364E35"/>
    
    <add name="ScriptResource" preCondition="integratedMode"
    
    verb="GET,HEAD" path="ScriptResource.axd"
    
    type="System.Web.Handlers.ScriptResourceHandler, 
    
    System.Web.Extensions, Version=3.5.0.0, Culture=neutral, 
    
    PublicKeyToken=31BF3856AD364E35" />
    
    <add name="MvcHttpHandler" preCondition="integratedMode"
    
    verb="*" path="*.mvc" type="System.Web.Mvc.MvcHttpHandler, 
    
    System.Web.Mvc, Version=1.0.0.0, Culture=neutral, 
    
    PublicKeyToken=31BF3856AD364E35"/>
    
    <add name="UrlRoutingHandler"
    
    preCondition="integratedMode" verb="*" path="UrlRouting.axd"
    
    type="System.Web.HttpForbiddenHandler,
    System.Web, 
    
    Version=2.0.0.0, Culture=neutral, 
    
    PublicKeyToken=b03f5f7f11d50a3a" />
    
    </handlers>
    
    </system.webServer>
    
    </configuration>
  4. Add the following to Global.asax.cs (create a Global.asax if you don’t already have one)
    public static void RegisterRoutes(RouteCollection
    routes)
    {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
    
    routes.MapRoute(
    "Default", //
    Route name
    
    "{controller}/{action}/{id}", //
    URL with parameters
    
    new { controller = "Home",
    action = "Index", id = "" } //
    Parameter defaults
    
    );
    
    }
    
    protected void Application_Start()
    {
    RegisterRoutes(RouteTable.Routes);
    }

    The above is the “standard” routing for a MVC site. It will work, but it might cause you trouble if you want the root of your website to still go to a ‘default.aspx’. Try this line instead:
    routes.MapRoute(
    "Default", 
    
    "MVC/{controller}/{action}/{id}", 
    
    new { controller = "Home",
    action = "Index", id = "" }
  5. Edit your .csproj file by hand
    <ProjectTypeGuids>{603c0e0b-db56-11dc-be95-000d561079b0};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>

    This will tell Visual Studio to act like this is an MVC project. You will get the context menu items for MVC in your Controllers and Views directories.
    Note: I’ve had trouble at this point with some Visual Studio installs. If you’re having trouble with “The project type is not supported by this installation.” messages here, it may be time for a clean install in a fresh VM.
  6. Set Up IIS. If you’re using IIS7, the System.Webserver settings in your Web.Config file should have taken care of this step. If you’re going to need to map the wildcard URL to aspnet_isapi.dll in order to get all of the MVC routing magic to work.
  7. You might want to copy of the /Views/web.config file from an existing MVC project to prevent your views from being viewed directly, rather than through their controllers.
  8. You might also wish to create a /Scripts folder and copy over the contents of the /Scripts folder from native MVC project. It has the jquery and MS AJAX javascripts that you may be looking for if you’re following a MVC book or tutorial.

Boy howdy. That was a good chunk of work. But now you have the infinite pleasure of developing in MVC, and your old web site should continue to work under you. Delicious incremental development goodness.

Resources

Adding ELMAH to an ASP.NET Site

ELMAH (Error Logging Modules and Handlers) is a fantastic library that provides error logging and troubleshooting support to an ASP.NET web site. You practically just drop it in and BOOM, you’ve got great exception reporting.

Step 1: Add A Reference

Add a reference to the ELMAH DLL in your ASP.NET project. (They tell me it just needs to be dropped in the BIN folder, but that almost seems like more work to me.)

Step 2: Add Config Sections

In web.config, add the following lines to <configSections>

<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler,
Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler,
Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler,
Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler,
Elmah"/>
</sectionGroup>
</configSections>

Step 3: Add the ELMAH Section

<elmah>
<security allowRemoteAccess="0" />
<errorLog type="Elmah.XmlFileErrorLog,
Elmah" logPath="|DataDirectory|" />
</elmah>

Step 4: System.web—httpModules and httpHandlers

<system.web>
<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule,
Elmah"/>
</httpModules>
<httpHandlers>
<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory,
Elmah" />
</httpHandlers>
</system.web>

Step 5: (II7 Only) Configure system.webServer

<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules>
<add name="Elmah.ErrorLog" type="Elmah.ErrorLogModule,
Elmah" preCondition="managedHandler" />
<add name="Elmah.ErrorFilter" type="Elmah.ErrorFilterModule" preCondition="managedHandler" />
<add name="Elmah.ErrorMail" type="Elmah.ErrorMailModule" preCondition="managedHandler" />
</modules>
<handlers>
<add name="Elmah" path="elmah.axd" verb="POST,GET,HEAD" type="Elmah.ErrorLogPageFactory,
Elmah" preCondition="integratedMode" />
</handlers>
</system.webServer>

Step 6: Secure remote access using ASP.NET membership

<location path="elmah.axd">
<system.web>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</location>

Resources:

IIS FTP 7 Provides Easy Virtual Host FTP

While it didn't ship with Server 2008, Microsoft added a new FTP Server for the new version off IIS. Unfortunately it is not an SFTP server, at best supporting FTP/S. However the new authentication options still make it a winner. You can provide a virtual FTP host for each web site you host with IIS 7. Just remember to login with [siteFQDN]|[username]...like 

erudition.radianttiger.com|josh
>

The pipey notation tells the server which vhost you belong to. Not elegant. Just way better than it was previously.

Next I'm excited to play with provider-based authentication. Having a pluggable architecture for logging in makes this FTP server really powerful.

Simple ASP.NET Custom Forms Authentication

So, I've been a really happy CSLA.NET user for a while, but I've been finding that generating the code has becoming tedious and my development velocity has been slowing down on my CSLA libraries. And so, rather than getting back involved with blood sacrifice, I figured I'd experiment with another layer in there. I'm currently working up some code with LLBLGen Pro (because llblgen lite is for suckers!), and it's working great....but Rocky made it so easy to do forms authentication with CSLA. It was just a couple'a pages in the book and it worked just fine and ....well.... I don't understand how it works. Now that I'm not including his framework, I need to figure it out.

So, follow along with me as we figure it out. Here's how it's done.

Step 1: Some configuration

First thing I tried was putting a Label on a form and assigning HttpContext.Current.User.ToString() to it. The result? "System.Security.Principal.WindowsPrincipal"

Right. My application is still out-of-the-box set to Windows authentication (Kerberos. ActiveDirectory. NSA Backdoor.) Let's change that.

<?xml version="1.0"?> <configuration> <system.web> <authentication mode="Forms" /> </system.web> </configuration>
>

Good. What's in that label now? "System.Security.Principal.GenericPrincipal" Much better.

Step 2: The Password is 'Joshua'!

This step is a big one. There are 27 members defined in the MembershipProvider contract, so go get some coffee, lithium, and a rotisserie chicken, and we'll get started.

Make a new class, call it MyMembershipProvider.cs. Make it extend MembershipProvider:

using System; using System.Web.Security; using System.Web; namespace Website1 { public class MyMembershipProvider : MembershipProvider
>

Go ahead and let Visual Studio define all of the inherited members. We're only going to change one:

public override bool ValidateUser(string username, string password) { return true; }
>

Now we've got another little configuration change to make.

<configuration> <system.web> <authentication mode="Forms" /> <membership defaultProvider="HscMembershipProvider"> <providers> <add name="MyMembershipProvider" type="Website1.MyMembershipProvider" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" applicationName="/" requiresUniqueEmail="false" passwordFormat="Clear" /> </providers> </membership> </system.web> </configuration>
>

Go head and drop a Login control and a LoginStatus on the form and try it out. It logs you in! No matter what you enter! Sure, you could put in some code that checks against your database to see if people are allowed in or not, but that's just elitist, isn't it? Creating an 'In-group' and an 'Out-group' and segregating them? Next thing, you'll be wanting to beat up blue eyed people because they prefer Lord of the Rings to Star Wars. Well, if you're going to put in some authentication code, go for it. I won't stop you.

Step 3: The Principal's Orifice

Ok. So there's a little gotcha at this point. If all you're looking for is a login and password check...then it's time to feed your brain to the zombies, 'cause you won't need it anymore. But if you want more than just a username logged in, we've go some stuff to do. Forms auth never gives you anything but the GenericPrincipal. It will be marked 'IsAuthenticated' if it is...and you can use the Roles provider to fill in the roles if you want. It also looks like it's possible to fill in the roles manually using an event handler in Global.asax.

But here's the thing. While I really dig on the login functionality of ASP.NET Membership, I'm not so convinced that Roles or Profiles provide a meaningful, robust implementation that's useful for more than a toy web forum. Can you give the branch manager for the Salem office full privileges to the customers there without letting him check on President Obama's secret pornography transactions at the Hillsboro branch? With just IsInRole(string)? It sure doesn't seem like it. The major advantage of Profiles seems to be that you can use web.config to define the fields....which is great until the point where you want to find all users within 50 miles of Denver....and then you're stuck in an abandoned warehouse with the vampires. Serialized XML doesn't query too very well. There's probably solutions to these problems, but not out-of-the-box and why work really hard to have a decoupled design with a provider model that is so customized and complicated to implement that you won't ever be swapping it out?

This is a job for a Custom Principal. That's what it is. Implement just enough to make the ASP.NET controls happy and then add the real meat to your own object. The principal also encapsulates an IIdentity object. You could go ahead and implement both of these, but since I'm lazy and want to go play Rock Band, I'm going to do both in one fell swoop. (Note that this is untested, and may put cracks in your Dilithium Crystals requiring embarrassing compromises with pimps.

using System; using System.Security.Principal; namespace Website1 { [Serializable()] public class MyPrincipal : IPrincipal, IIdentity { public static MyPrincipal Login(string username, string password) { MyPrincipal loginUser = new MyPrincipal(); if (!string.IsNullOrEmpty(password)) { loginUser.Name = username; loginUser.IsAuthenticated = true; return loginUser; } return loginUser; } public bool RussianRoulettePermission() { Random random = new Random(); if (random.Next(1, 6) == 3) return false; return true; } #region IPrincipal Members public IIdentity Identity { get { return this; } } public bool IsInRole(string role) { return false; } #endregion #region IIdentity Members public string AuthenticationType { get { return "Custom"; } } public bool IsAuthenticated { get; private set; } public string Name { get; private set; } #endregion } }
>

I accept that you and your fascist friends will probably want to do more password checking than making sure that the user typed a letter. Good fences make good neighbors, right? The real point is that you can put your own Crazy User Code in here and have it available when your pages want to use it.

Now we can fix the Membership Provider to use our new shiny principal object!

public override bool ValidateUser(string username, string password) { HttpContext.Current.User = MyPrincipal.Login(username,password); return HttpContext.Current.User.Identity.IsAuthenticated; }
>

Give it a try!

Step 4: Dammit!

I set Current.User to my Principal. I did. I did. I did. Why doesn't my damn label change from GenericPrincipal when I log in? It's cheesin' me off here!

The problem here is the Forms Authentication uses it's own principal and sets the context to it on each page load. This is what it's supposed to do, but we want better! We're going to add a few methods and fix the problem. One caveat here (and why do people want to eat caves? And which Cave are you At when that happens? Where's my lithium?): I'm going to trust the forms authentication cookie. I think that's enough of a ticket to bypass password authentication. If you don't, you might want to jump through some extra hoops. I'm also trusting that my developers won't use the bypass methods to create secret login backdoors and the like. I figure if you can't trust the coders then...well, you work at my office. But anyways....

To the principal!

public static MyPrincipal GetLoggedInUser(string username) { MyPrincipal loginUser = new MyPrincipal(); loginUser.Name = username; loginUser.IsAuthenticated = true; return loginUser; } public static MyPrincipalGetLoggedOutUser() { return new MyPrincipal(); }
>

And in Global.asax!

protected void Application_AcquireRequestState(object sender, EventArgs e) { if (User.Identity.IsAuthenticated) { HttpContext.Current.User = MyPrincipal.GetLoggedInUser(User.Identity.Name); } else { HttpContext.Current.User = MyPrincipal.GetLoggedOutUser(); } }
>

Step 5: And now for a magic trick.

Happy Day! Kill Ugly One-Horned Mule!

My cheap and dirty Membership implementation works. Now I can put this in my Form_Load method:

Label1.Text = "You're Logged in: " +HttpContext.Current.User.Identity.IsAuthenticated + " And you're dead!: " + ((MyPrincipal)HttpContext.Current.User).RussianRoulettePermission();
>

Sweet! You can find out if you're logged in. And if you're dead. All at the same time.

Next Steps: Drunk on Cookie Magic

Obviously we're going to need some code to check passwords. Probably fetching against the database. No problem. I'm planning on encapsulating my DTOs from LLBLGen into my principal. That way I can expose read access to user objects for the logged-in users. I can also put in complex permissions right there. I'm going it my way. Sweet!

It's worth noting that this implementation goes off to the DB for each page load. That can sure suck up the performance points under a real load. However, a little bit of effort can store the principal in the session object and then you get it back, deserialized, instead of requiring a round-trip. Just remember to code for the case where the session expires, but the forms cookie does not.

 

until next time, remember: the skin is the best part.