Tuesday, July 17, 2007 -
First things first: I managed to do a really boneheaded maneuver for the starter site and left New Page creation open to anyone who can guess the URL. No excuses on my part - that was plain stupid. Anyway:
if a user goes to /view/newpage.aspx on the starter site they can add pages to your site (if you're using the Starter Site).
You can fix this by changing lines 55 through 60 in PageView.aspx.cs to:
if (SiteUtility.UserCanEdit()) { SetupNewPage(); this.Title = "Create a New Page"; } else { Response.Redirect("~/login.aspx"); }
I've fixed this issue and am now loading the new site (2.0.3a) up to CodePlex. Feel free to call me names - I deserve it.
The Good News
For a long time people have been asking about architectural approaches to using SubSonic, and I usually always try to champion the MVC patter since I like it so much. The problem comes in when you consider how ASP.NET is built (with the WebForm/CodeBehind model) and how ActiveRecord can fall flat on its face in this model.
For instance - it's all too tempting to spread your logic out through an application (in the code behind) and not use a central controller (aka "Business Facade") when you're using ActiveRecord since it's really simple to do something like:
MyGridView.DataSource=Product.FetchAll(); MyGridView.DataBind();
This approach is handy for rapid development, but in some cases it can lead to sloppiness.
Enter The MVC Template Set
So this last week I had at our Code Generator and our templates, and I rolled out a nice MVC-ready set of templates that will "dumb down" the objects and collections, and force you to push everything through a Controller class (which is generated for you).
So now instead of calling "product.Save("me")" you have to do this:
Product product = ProductController.Get(newID);
product.ReorderLevel = 100;
ProductController.Save(product,"unit test");
Collections won't load themselves either, instead you need to use the new "List()" command:
ProductCollection coll = ProductController.List();
You can also pass in a Query to the controller, which will load the list using the passed-in Query:
ProductCollection coll = ProductController.List(ProductController.Query()
.WHERE(Product.Columns.CategoryID,5));
This makes the loading of the collections a lot more flexible :).
You can also pass in a Reader if you like to do the same thing:
IDataReader rdr = new Query(Product.Schema).ExecuteReader();
ProductCollection coll = ProductController.List(rdr);
The general idea here is that just about all data-related interaction has been pushed to the controller class (which are based on our current ODSControllers) and there is almost no logic in any of the generated objects.
All of the templates are partials, and the idea here is that you can add on to them and keep all your logic in one place (you don't have a choice really).
I want to stress that we are NOT changing anything with the default stuff here - if you like ActiveRecord and want to keep using SubSonic like you always have been, it will be there for you. If you want to have a bit of the MVC architecture done up for you up front - check out these templates!
You can download them from here.
Setting Them Up
It's not very difficult to switch over if you want to use these guys. I should mention first off that I haven't committed anything to source yet, so you won't see the changes in the trunk. I have added my dev build of sonic.exe and SubSonic.dll to the download file, so you'll need to make sure you use those to power up the new templates (sonic.exe if you generate them).
To use the new templates, just change the templateDirectory setting in the SubSonicService to point to the new template set (I put mine in the SubSonic install directory):
<SubSonicService defaultProvider="Northwind" enableTrace="true" templateDirectory="C:\Program Files\SubSonic\SubSonic 2.0.3\Templates\MVC"> <providers> <clear/> <add name="Northwind" type="SubSonic.SqlDataProvider, SubSonic" connectionStringName="Northwind" generatedNamespace="Northwind"/> </providers> </SubSonicService>
And that's it. Your new templates will kick up for you! I'm anxious to hear feedback and improvement ideas - so do let me know what you think!
That is, the Controller is responsible for knowing what kind of data it needs to get and will populate the Model(s) with that data - applying any applicable business rules along the way. However, the controller typically uses some kind of data-broker, like a data service, to actually go out and get said data.
So, it seems like the SubSonic controllers actually play the part of the data-broker used by the MVC Controllers. I'm just trying to make sure we're not muddying the MVC waters by using the term 'Controller' in too loose a fashion.
I could be way off here - and feel free to call me names if I am, after all I did just spend 2 straight hours watching Entourage and sucking down some fine domestic pilsners.
But, the point you raise is one that i've been musing on a lot. Typically a controller only exposes methods it needs to, and no more. I was toying with making all the contoller stuff internal, so you HAD to write your business methods on top of the controller partial. In other words you couldn't call ProductController.Save() from anywhere :).
Rails enforces this by routing all app flow through the controller first and last... we don't have this luxury so to get it right (and keep the lovely ease of use we've created with SubSonic) is going to be tricky...
Blah blah blah - I really want to get this right :) so I really would love any input. I like the point you raise and maybe it makes sense to "hide" these methods (Save, Insert, Update, List, etc) from the rest of the app (make them protected), forcing the user to create methods specific to the app's needs. Thoughts?
This is very much a work in progress so let me have it!
I then write code (including tests) against these interfaces.
This makes unit testing a breeze because I can create mocks (using Rhino Mocks) of the controller interface and test that methods are called on it when my MVP/MVC code executes.
The problem is when subsonic regenerates the code I have to alter the compiled controllers to implement the interfaces, would be great if this was done automatically.
Great concept. I totally agree with having the option to make the object methods internal and force access to those methods. I've seen the problem first hand the with exposing public methods for something like Save and not have it go through the coded business logic. But I do think it should be optional, I know one of my biggest gripes about Rails is that you can only do it one way, anything else leads to pain and suffering.
MrTea,
You can download the source and edit the templates to automatically add your interfaces to the controller, not to mention the Source Code for SubSonic is definitely worth looking over.
I've been toying around with templates for MVP pattern that may be of interest to you.
And it would/could work with your requirement to enforce the user to expose only what the application requires.
1. The Rails Way. Obviously ActiveRecord is Rails-inspired and when you're in Rails the Controller doesn't have the logic to save, it knows how to call the Save() method on the model. So really if you were trying to get some Controller templates going you would keep the original templates and just use the Controller to trigger a Save(). Which in turn means your controller templates are just empty classes :)
2. The "classic" data broker pattern. This to me is what .netTiers does and it's the reason I've been looking at other frameworks. In attempting to make it "easier" and fit more of a pattern it adds many layers that my small to medium projects don't really need.
Personally I like teaching good architecture through examples, so perhaps instead of trying to move things around in the SubSonic templates create a "MVC Example" solution, showing a solid MVC pattern in use. Something that actually shows a Controller populating a View's (WebForm) controls and also pulling the values back from the View, repopulating the Model (SubSonic created objects) and the Controller calling the Model's Save() method.
So while I agree completely that MVC is a much better pattern and we should do what we can to push it, I think time would be better spent creating an example of one in use, with "real" Controller functionality. If you generate a Controller with CRUD novices are going to start thinking that's what controllers are for, hence confusing them. Also, right now a lot of the SubSonic examples show a straight DataBind, why not get rid of all of those and show by convention and example, rather than "force".
Humm.. I see the ODS Controllers much more as DAOs than as MVC Controllers. Also, if you change the visibility of the ODS Controllers methods to protected they cannot be used by/with the ASP.Net ObjectDataSource. I think you need another XXX Controler that will act as the MVC Controller, and will use the ODS Controllers to get/update data. What do you think?
@MrTea:
I had a similar problem and opened an issue at codeplex: http://www.codeplex.com/actionpack/WorkItem/View.aspx?WorkItemId=10477
Broadly speaking I'm in agreement with Shawn Oster.
Personally I'd like to see more documentation and greater scope in the template facilities. Then (fingers crossed) we'll see an expansion of community driven templates, which can implement different derivatives of MVP and MVC.
@Rob,
If you're looking to build an MVC solution with templates, I would suggest you try and follow the pattern evangelically. That way you will identify and address any potential short comings in the template system as you implement an intense pattern and offer a great example for others to work from if they want a lesser implementation.
@Agrinei,
Regarding work item 10477, I thoroughly endorse your suggestion and have voted for it. The more people who vote for that work item I would suggest would be a interesting indication as to how many people are looking to implement SubSonic in an Enterprise environment.
Good look Rob and thanks again.
Full disclosure: I don't use subsonic, but I do try to use MVP, and I have in the past used "Managers" to load / save business objects (EmployeeManager.Save(empl)). But I never saw that choice as being part of a broader choice as to if I would implement MVP or not.
Can anyone elighten me?
@MrTea
Are you sure you mock the Controller and not the View?
Every example I have seen, and the way I do it as well, is to mock the View, which is tightly coupled to asp.net, and thus very hard to unit test.
If you DO mock the controller, how do you do it? Your view would be filled with webcontrols and other asp.net framework stuff as it is dealing with the UI right? How do you do that?
I'm probably going to post something more on this, but for now... some responses:
@All: One thing I think is missing in this is that the Controller class methods are all virtuals, and the class itself is partial. So the concept here is that the first thing you'll do is override the core methods with your own logic.
So while it's purely true that the controller classes (as i've schemed them currently) are essentially data brokers, when you override (or hide, whatever) and then implement your logic on them, they become much more of a controller in a sense.
Now we don't have the luxury of punching events through contollers ---- YET. If you notice, the contoller methods comply with Rails RESTFul architecture (7 methods per controller) to a degree - but are minus the "Index" and "Show" methods because as of now, we can't route events through the controller.
That's going to change soon when someone writes up a real nice web MVC pattern that's simpler than Castle.
@Shawn: yep good points and I'd love to work up an example.
@Ed: Eric and I have been talking about expanding the capabilities of the code tool. We have to make sure we stay on target in terms of what we're offering - it's not just another code gen tool :). I'm tempted to just run up some CodeSmith templates cause it's easier :).
@Chris May: some people like to run logic through a central point (I left a comment on your blog thus). You can make that central point be in your model (or "Product") class, but architecturally speaking logic doesn't belong there. A lot of people would suggest even knowledge of the DB doesn't belong there.
I replied (to your reply), and I think maybe this is where I was misunderstanding:
"So while it's purely true that the controller classes (as i've schemed them currently) are essentially data brokers, when you override (or hide, whatever) and then implement your logic on them, they become much more of a controller in a sense."
If I understand, the controller will have data broker ability, but you are encouraged to build out this class in another partial class definition to override/modifiy the functionality you need to make the controller become more of a page controller.
To accomplish this is no simple feat and you just want a sounding board yeah?
Thanks. I'm surprised I missed that post from Ayende.
I think Rob explained my approach.
Basically I have a presenter, view and model. The view is an interface usually implemented by an ascx page.
The model is usually a lightweight class (again based on an interface) which generally interfaces with the domain, and any other services it requires.
Typically, those services are injected using constructor dependency injection.
I see the subsonic controller interfaces as a service (data service, or data access object) and my model calls methods on that service.
The obvious disadvantage with all this is the seeming complexities it introduces to often "simple" programming tasks.
The obvious benefit, is that I can focus unit tests on the presenter (which will use mocks of the data service, model and view) or on concrete implementations of the data service and model.
(ps this is all very new to me, and I must admit I have spent weeks/months trying to get my head around it all, I still don't "get" all the subtleties involved)
Great work... I have found by adding controllers to the mix I can abstract my third party developer from having direct access to my DB.
I recently have been asked to create a API, and the controllers here make it perfect to be able to restrict the developer to say only have access to data in a certain category...
I would like the same access with the REST interface... which I guess I can do if I subclass the Subsonic REST handler, and introduce my restrictions there.
Just wanted to tell you that the VB templates don't generate the correct subclassing. I used the same Northwind db to create a both VB and c# MVC class libraries. The c# code worked; the VB code did not.