Yeah. you can put a Controller class anywhere, even the root if you like, but i like \Controllers\Controls for a controller which renders a control as opposed to a page.
Then when you want to render your control, you don't use Html.RenderUserControl, what you do is call a method on a controller. I've written a helper method to do this but it's a bit of a hack. I'll show you my first attempt and then what that turned into.
The following will be called like - <% Html.Call<MyController>( c => c.Index() ); %>
1 public static void Call<TController>(this HtmlHelper helper, Action<TController> method) where TController : class, IController
2 {
3 IHttpContext context = helper.ViewContext.HttpContext;
4 RouteData routeData = helper.ViewContext.RouteData;
5 RequestContext requestContext = new RequestContext( context, routeData );
6
7 TController controller = ControllerBuilder.Current.CreateController( requestContext, typeof( TController ) ) as TController;
8
9 if( controller == null )
10 return;
11
12 method( controller );
13 }
Fairly simple and easy to read. new up a request context, passing in the current values. Build a controller and call the action. But obviously this doesn't work. It looks in the wrong folder for the view. the controller context is null. and TempData is null.
So, we need to:
copy the route data without modifying the original.
set the new controller name in the route data.
new up a controller context and pass that to the controller (This step prevents us from being able to act on an IController, and only works for instances of Controller, which is a shame).
And pass TempData to the controller, even tho the setter is not public...
Here's the code.
1 public static void Call<TController>(this HtmlHelper helper, Action<TController> method) where TController : Controller
2 {
3 IHttpContext context = helper.ViewContext.HttpContext;
4 RouteData routeData = new RouteData();
5
6 // Copy RouteData
7 foreach( string key in helper.ViewContext.RouteData.Values.Keys )
8 {
9 routeData.Values[key] = helper.ViewContext.RouteData.Values[key];
10 }
11
12 // Replace controller name
13 string controllerName = typeof( TController ).Name;
14
15 if( controllerName.EndsWith( "Controller", StringComparison.OrdinalIgnoreCase ) )
16 controllerName = controllerName.Remove( controllerName.Length - 10 );
17
18 routeData.Values["controller"] = controllerName;
19
20 // Create the controller
21 RequestContext requestContext = new RequestContext( context, routeData );
22 TController controller = ControllerBuilder.Current.CreateController( requestContext, typeof( TController ) ) as TController;
23
24 if( controller == null )
25 return;
26
27 // Set ControllerContext event tho CreateController had enough information to do so.
28 controller.ControllerContext = new ControllerContext( requestContext, controller );
29
30 // HACK HACK HACK
31 PropertyInfo tdProp = typeof( TController ).GetProperty( "TempData" );
32 tdProp.SetValue( controller, helper.ViewContext.TempData, null );
33
34 // Call method.
35 method( controller );
36 }
Another way to do this would be to change Action<TController> to Expression<Action<TController>> and build up the route data from that and then call Execute on the controller. But i think my first attempt is Ideal. It would be great if this was possible in a future release.