| Join
Rate It (1)
Last post 03-04-2008 3:57 AM by rjcox. 20 replies.
Sort Posts:
Oldest to newest
Newest to oldest
Hello Folks, In a previous post, I asked where to place reusable controls which i use on many pages. The answer i got was Views\Shared. So if i have my own custom logon control, it might be Views\Shared\Logon.ascx
But what about the LOGIC for a reusable web user control? does that go in a 'controller'? If so, where does this controller go? \Controllers\Shared\LogonController.cs
Thanks guys for helping a newbie out :)
pure.krome:But what about the LOGIC for a reusable web user control? does that go in a 'controller'? If so, where does this controller go? \Controllers\Shared\LogonController.cs
That would be the obvious place, but in the end the answer will be given by the design of your web application.
Ask yourself what should happen when a user successfully logs on? Or fails? Or logs off? What should be shown when a user is already logged on?
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, IController2 {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 : Controller2 {3 IHttpContext context = helper.ViewContext.HttpContext;4 RouteData routeData = new RouteData();5 6 // Copy RouteData7 foreach( string key in helper.ViewContext.RouteData.Values.Keys )8 {9 routeData.Values[key] = helper.ViewContext.RouteData.Values[key];10 }11 12 // Replace controller name13 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 controller21 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.
Holly Flying Toasters, Batman! tgmdbm, that's really got me confused :( In a nutshell, are you saying that Out Of the Box, this MVC doesn't handle controlling 'postback' actions for MVC Web User Controls?
You can just cut and paste the code into a static class and just use <% Html.Call<ErrorsController>( c => c.Show() ); %> to run your controller actions (be careful, these actions should only render controls, not pages, or you'll end up with whole pages inside your main page. not good)
You shouldn't need be concerned with how it works (assuming it does work, i haven't extensively tested it). It should come out of the box, but you're right, it doesn't!
Note: Rob Conery has created a new extension method called RenderAction, which does just this, which will be in the next drop of the MVC Toolkit.
Here's a simple example.
Controllers+-- HomeController.cs
[ControllerAction]
public void Index() { RenderView( "Index" ); }
Views+-- Home +-- Index.aspx
<% Html.Call<WidgetController>( c => c.Show() ) %>
Controllers+-- Controls +-- WidgetController.cs
public class WidgetController : Controller
{
[ControllerAction]
public void Show()
{
ViewData["widgetData"] = // get data from somewhere
RenderView( "Widget" );
}
}
Views+-- Widget +-- Widget.ascx
<%= ViewData["widgetData"] %>
<%= ViewData["widgetData"] %>
pure.krome, did you get anywhere with this?
Yeah - i've had a bit more luck with MVC View Web Controls. Firstly, i need to thank BEN SCHEIRMAN for helping me out, here. he really got the ball rolling.
What i ended up doing (and it was really getting my head around what an MVC View User Control _really_ is, compared to my previous incorrect understanding that a VUC has some logic associated with it) was the following.
Index method/action in the HomeController:- That sets up _all_ the ViewData, for both the html stuff on the Index.aspx 'view' and for the ProductsList VUC that exists in that view. Now, because the default Index action is being called on the HomeController, all the correct view data stuff for itself AND it's children (ie. the index view knows that it has some children view user controls) are being setup there. When it came to rending the specific data on that ProductList VUC (remember, the index view asks to RenderUserControl(..)), the viewdata already exists in that child view user control being rendered.
as such, no vuc controller stuff was ever need.
I'm not sure if this is the right method, though .. but this was what i sorted figured out on the weekend and this week during work i've been spending all my time getting this in action.
I used to think the View User Control as a completly seperate reusable component .. where u could box it up into a dll or something and give it to whoever -- just like any commerical webform component out there. It's not. The _VIEW_ is completly reusable, but not the logic. The logic - which in this case is getting a list of products and then adding that to the ViewData - needs to be handled on every view where this reusable view user control exists.
That said, i've put the static method that impliments this logic to the code behind of the view user control .. so the logic is in one place. the controllers (ie. the Index 'action' in the HomeController) all call this one method and bobs your uncle.
i'll try and draw a map of what the code looks like.
+Views\Home\Index.aspx <-- The main page view. This has a single View User Control inside this view, called ProductList.ascx.+\Views\Shared\ProductList.ascx <-- reusable view user control. This displays a unordered list of products.+\Views\Shared\ProductList.ascx.cs <--- static method :: public static void PrepareViewData( ... the objects that the view user control requires) { .. }+\Controllers\HomeController.cs <-- Index method calls the static PrepareViewData(..) method.
done. Does that sound like i'm close?
You can do it like that but you're breaking encapsulation.
pure.krome:+\Controllers\HomeController.cs <-- Index method calls the static PrepareViewData(..) method.
I have a real problem with this, you are tying the Controller to the View (by calling that static method) and you shouldn't do that. You're also tying the View to the Model and that's a HUGE no no (I'm assuming the static method, PrepareViewData, is calling to the Model to get the list of products??).
As you've already pointed out this is not very DRY. You need to do the same thing for every ViewPage which contains this control.
In my example above you can bundle up the WidgetController and the Widget and pass that to anyone to reuse. I'll rewrite the example to better suit your naming, and since Rob has created the RenderAction method i'll use that.
[ControllerAction]public void Index() { RenderView( "Index" ); } // note we don't have to get any products here.
<% Html.RenderAction<ProductListController>( c => c.Show() ) %>
Controllers+-- Controls +-- ProductListController.cs
public class ProductListController : Controller{ [ControllerAction] public void Show() // this gets called by Html.RenderAction { ViewData["productsList"] = db.Products; // get data from somewhere RenderView( "ProductList" ); }}
Views+-- ProductList (or Shared) +-- ProductList.ascx
<ul><% foreach(Product p in ViewData["productsList"]) { %> <li><%= p.Name %></li><% } %></ul>
As you can see all we have to do is call Html.RenderAction from within any view which wants a ProductList and hey presto. The logic is encapsulated in the ProductListController and we have very clear separation of concerns. And no need for any codebehind.
A beautiful solution.
DOL (drool out loud)!! this is so kewl. brb! Thanks heaps tgmdbm.
* pure.krome runs off to refactor my lame code.
hmm. this might sound really noobish .. but where is the RenderAction code? i can't find it anywhere (well, any official comment / code).
just when i thought i was there!
tgmdbm:
Views+-- Home +-- Index.aspx <% Html.RenderAction<ProductListController>( c => c.Show() ) %>
pure.krome:
hmm. this might sound really noobish .. but where is the RenderAction code? i can't find it anywhere (well, any official comment / code).
just when i thought i was there!
sliderhouserules:There is some elegance and simplicity to this, but you don't think the above is clearly tying the view to the controller, which is all the bad that you're decrying about tying the view to the Model? How is strongly typing (generic form) a View page not tying it to the Model, anyway?
In this case you are tying the view to a controller, but the point is it's the controller for a Control, so I don't see that as any different from using a regular Control. Also there should be an overload of this which simply takes the name of the controller and action, which obviously isn't tied to anything.
As for the View being tied to the Model, that's not the case at all. The View is tied to the data which the Controller gives it. It's perfectly viable to create restrictions of the model classes to pass to the view. something akin to ViewProduct x = new ViewProduct(product). This could contain less fields than the model class and then you've created a very clear separation. The fact is that it's usually appropriate for the View to have access to most of the fields of the Model classes and so creating these intermediary classes are a seen as a waste of time.
The emphasis however is on not calling ANY methods on ANY of the Model classes from within the View! Only accessing properties!
As for an implementation of the RenderAction helper method, you can just take my Call code from earlier and change its name. It works fine for me, let me know if yo find a bug in it.
Good luck
tgmdbm, nice solution. My only concerns are
1. Some digging is necessary to realize that <% Html.RenderAction... %> renders the "Widget" ProductList.ascx.
2. ViewData["productsList"] is fragile, we have to make sure that all pages that use the widget do not overwrite this value.
3. "ProductListController" is a controller for the model..."ProductList"? As pointed out earlier, it is not a Controller - Model couple in the traditional sense that the Model is a domain object.
Other than that it is a nice solution to the "I need to bundle view + logic into reusable unit" problem that one might encounter sometimes. Great work.
Advertise |
Ads by BanManPro |
Running IIS7
Trademarks |
Privacy Statement
© 2009 Microsoft Corporation.