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 :)
:: Never underestimate the predictability of stupidity ::
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() ); %>
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.
public static void Call<TController>(this HtmlHelper helper, Action<TController> method) where TController : Controller { IHttpContext context = helper.ViewContext.HttpContext; RouteData routeData = new RouteData();
// Create the controller RequestContext requestContext = new RequestContext( context, routeData ); TController controller = ControllerBuilder.Current.CreateController( requestContext, typeof( TController ) ) as TController;
if( controller == null ) return;
// Set ControllerContext event tho CreateController had enough information to do so. controller.ControllerContext = new ControllerContext( requestContext, controller ); // HACK HACK HACK
PropertyInfo tdProp = typeof( TController ).GetProperty( "TempData" ); tdProp.SetValue( controller, helper.ViewContext.TempData, null ); // Call method.
method( controller ); }
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?
:: Never underestimate the predictability of stupidity ::
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.
[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" );
}
}
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?
:: Never underestimate the predictability of stupidity ::
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.
Controllers
+-- HomeController.cs
[ControllerAction]
public void Index() { RenderView( "Index" ); } // note we don't have to get any products here.
Views +-- Home +-- Index.aspx
<% Html.RenderAction<ProductListController>( c => c.Show() ) %>
public class ProductListController : Controller { [ControllerAction] public void Show() // this gets called by Html.RenderAction { ViewData["productsList"] = db.Products; // get data from somewhere RenderView( "ProductList" ); } }
<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.
pure.krome
Member
532 Points
349 Posts
Web User Controls + Controllers?
Jan 20, 2008 12:17 AM|LINK
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 :)
rjcox
Contributor
7064 Points
1444 Posts
Re: Web User Controls + Controllers?
Jan 20, 2008 07:09 AM|LINK
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?
tgmdbm
Contributor
4392 Points
883 Posts
ASPInsiders
MVP
Re: Web User Controls + Controllers?
Jan 20, 2008 11:07 AM|LINK
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() ); %>
public static void Call<TController>(this HtmlHelper helper, Action<TController> method) where TController : class, IController { IHttpContext context = helper.ViewContext.HttpContext; RouteData routeData = helper.ViewContext.RouteData; RequestContext requestContext = new RequestContext( context, routeData ); TController controller = ControllerBuilder.Current.CreateController( requestContext, typeof( TController ) ) as TController; if( controller == null ) return; method( controller ); }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.
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.
Framework suggestion Controller
pure.krome
Member
532 Points
349 Posts
Re: Web User Controls + Controllers?
Jan 21, 2008 03:31 AM|LINK
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?
tgmdbm
Contributor
4392 Points
883 Posts
ASPInsiders
MVP
Re: Web User Controls + Controllers?
Jan 21, 2008 05:57 AM|LINK
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.
http://blog.wekeroad.com/2008/01/07/aspnet-mvc-using-usercontrols-usefully/
tgmdbm
Contributor
4392 Points
883 Posts
ASPInsiders
MVP
Re: Web User Controls + Controllers?
Jan 22, 2008 02:36 PM|LINK
Here's a simple example.
Controllers
+-- HomeController.cs
[ControllerAction] public void Index() { RenderView( "Index" ); }Views
+-- Home
+-- Index.aspx
Controllers
+-- Controls
+-- WidgetController.cs
Views
+-- Widget
+-- Widget.ascx
tgmdbm
Contributor
4392 Points
883 Posts
ASPInsiders
MVP
Re: Web User Controls + Controllers?
Feb 13, 2008 11:02 PM|LINK
pure.krome, did you get anywhere with this?
pure.krome
Member
532 Points
349 Posts
Re: Web User Controls + Controllers?
Feb 14, 2008 02:49 AM|LINK
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?
tgmdbm
Contributor
4392 Points
883 Posts
ASPInsiders
MVP
Re: Web User Controls + Controllers?
Feb 14, 2008 03:21 AM|LINK
You can do it like that but you're breaking encapsulation.
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.
Controllers
+-- HomeController.cs
[ControllerAction] public void Index() { RenderView( "Index" ); } // note we don't have to get any products here.Views
+-- Home
+-- Index.aspx
Controllers
+-- Controls
+-- ProductListController.cs
Views
+-- ProductList (or Shared)
+-- ProductList.ascx
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.
pure.krome
Member
532 Points
349 Posts
Re: Web User Controls + Controllers?
Feb 14, 2008 03:49 AM|LINK
DOL (drool out loud)!! this is so kewl. brb! Thanks heaps tgmdbm.
/me runs off to refactor my lame code.