Are the Drivers and Controllers in Orchard somewhat the same? - asp.net-mvc

I'm creating a new widget in the Orchard CMS. The way I do this is by adding a Route and Controller first and try out the functionality by running the code on an url defined in the Route, like http://localhost:30320/Index
My Routes.cs is set up so it routes this request to the specified controller and stuff is happening on the screen. This all works quite well.
Now that I'm happy with the result I tried placing all of this in a new Widget. For this I've created new Migrations class which sets up the widget. This is fairly straightforward and now the widget has been added to the Homepage layer.
The thing I'm running into is the Controller isn't executed anymore. Not very strange as I haven't set up any routes which specify the Controller should be executed. I'm wondering, should I move the Controller logic to the Driver method, so the View still gets the necessary information?
The driver I've got at the moment looks like this:
public class FrontpageDrivers : ContentPartDriver<FrontpageModelPart>
{
protected override DriverResult Display(FrontpageModelPart part, string displayType, dynamic shapeHelper)
{
//return base.Display(part, displayType, shapeHelper);
if (displayType.StartsWith("Detail"))
return ContentShape("Parts_Index", () => shapeHelper.Parts_Index(
LatestPostCollection: part.LatestPostCollection,
TopRatedPostCollection: part.TopRatedPostCollection,
TotalMonthCollection: part.TotalMonthCollection,
ContentPart: part
));
return null;
}
}
The PartsController method which needs to be executed looks like this:
[HttpGet]
public ActionResult Detail()
{
//Do something to get blogposts
var getter = new GetBlogPost(_blogService, _blogPostService, _votingService);
getter.Initialize();
var latestPosts = getter.GetLatestPosts();
var highestRankedPosts = getter.GetHighestRankedPosts();
var archiveData = getter.GetTotalPostsPerMonth();
var viewModel = new FrontpageModelPart();
viewModel.LatestPostCollection = latestPosts;
viewModel.TopRatedPostCollection = highestRankedPosts;
viewModel.TotalMonthCollection = archiveData;
return View("Index", viewModel);
}
I've tried renaming the method to Index and Detail, both won't do the trick.
The view which is shown is /Views/Parts/Index.cshtml. If I put some static text in the file, I can see this view is being rendered correctly.
So, should I move the Controller logic to the Driver, or am I forgetting something in the setup?
Note: I've got the placement, module, migrations and handler in place already.
Edit:
If I'm using this code, everything works quite well:
protected override DriverResult Display(FrontpageModelPart part, string displayType, dynamic shapeHelper)
{
var controller = new PartsController(Services, _blogService, _blogPostService, _votingService);
part = controller.GetIndexViewModel();
if (displayType.StartsWith("Detail"))
return ContentShape("Parts_Index", () => shapeHelper.Parts_Index(
LatestPostCollection: part.LatestPostCollection,
TopRatedPostCollection: part.TopRatedPostCollection,
TotalMonthCollection: part.TotalMonthCollection,
ContentPart: part
));
return null;
}
Even though this works quite well, it just feels like 'hacking' to me...

Use controllers when you want to take over everything that appears in the Content zone, but return a shape result so that the theme, widgets, etc can still chime in. That's what your controller fails to do.

Related

ASP.NET MVC 4, how to access/modify the view model object (and change view and action method) before it is used as action method parameter?

Is there any useful hook in ASP.NET MVC (MVC4) which can let you access the Action method parameter (View model) before the action method becomes invoked, and then also (e.g. depending on the value of something you checked in the action method parameter) let you prevent the action method from being invoked, i.e. instead either forward the view model object (action method parameter) to another action method or directly to some view (i.e. without any further processing in an action method) ?
If you do not understand the question, please see the code example below which should illustrate the kind of code I am looking for...
(though I do not know if there actually exists such kind of interface and a possibility to hook an implementation into the MVC framework)
If this is indeed possible, I would like to see an answer with code example about how to do it (and not just a response with someone claiming that e.g. "try using method 'ActionFilterAttribute.OnActionExecuting' or 'IModelBinder.BindModel' " because I have already tried those and could not make it work).
Also, please respect that I do not want this thread to become a discussion about WHY to do it, but want to see HOW to do it.
(i.e. I am not interested in getting into discussions with responses such as "What are you actually trying to achieve?" or "There are probably better things of doing what you want to do...")
The question can be split into three subquestions/code examples as my own code samples below try to illustrate:
(but would like them "refactored" into REAL code with usage of real existing types)
(obviously, every type below which includes the substring "Some" is something I have made up, and I am looking for the corresponding real thing ...)
(1) Example of how to get access to (and potentially modify) view model objects (action method parameters) in a generic place before the actual action method is invoked with the view model object parameter.
The kind of code example I am looking for would probably be similar to below but do not know what kind of interface to use and how to register it to be able to do something like below:
public class SomeClass: ISomeInterface { // How to register this kind of hook in Application_Start ?
public void SomeMethodSomewhere(SomeActionMethodContext actionMethodContext, object actionMethodParameterViewModel) {
string nameOfTheControllerAboutToBeInvoked = actionMethodContext.ControllerName;
string nameOfTheActionMethodAboutToBeInvoked = actionMethodContext.MethodName;
// the above strings are not used below but just used for illustrating that the "context object" contains information about the action method to become invoked by the MVC framework
if(typeof(IMyBaseInterfaceForAllMyViewModels).IsAssignableFrom(actionMethodParameterViewModel.GetType())) {
IMyBaseInterfaceForAllMyViewModels viewModel = (IMyBaseInterfaceForAllMyViewModels) actionMethodParameterViewModel;
// check something in the view model:
if(viewModel.MyFirstGeneralPropertyInAllViewModels == "foo") {
// modify something in the view model before it will be passed to the target action method
viewModel.MySecondGeneralPropertyInAllViewModels = "bar";
}
}
}
}
(2) Example of how to prevent the targeted action method from being executed and instead invoke another action method.
The example might be an extension of the above example, with something like below:
public void SomeMethodSomewhere(SomeActionMethodContext actionMethodContext, object actionMethodParameterViewModel) {
... same as above ...
if(viewModel.MyFirstGeneralPropertyInAllViewModels == "foo") {
actionMethodContext.ControllerName = "SomeOtherController";
actionMethodContext.MethodName = "SomeOtherActionMethod";
// The above is just one example of how I imagine this kind of thing could be implemented with changing properties, and below is another example of doing it with a method invocation:
SomeHelper.PreventCurrentlyTargetedActionMethodFromBecomingExecutedAndInsteadExecuteActionMethod("SomeOtherController", "SomeOtherActionMethod", actionMethodParameterViewModel);
// Note that I do _NOT_ want to trigger a new http request with something like the method "Controller.RedirectToAction"
}
(3) Example of how to prevent the normal action method from being executed and instead forward the view model object directly to a view without any further processing.
The example would be an extension of the first above example, with something like below:
public void SomeMethodSomewhere(SomeActionMethodContext actionMethodContext, object actionMethodParameterViewModel) {
... same as the first example above ...
if(viewModel.MyFirstGeneralPropertyInAllViewModels == "foo") {
// the below used razor view must of course be implemented with a proper type for the model (e.g. interface 'IMyBaseInterfaceForAllMyViewModels' as used in first example above)
SomeHelper.PreventCurrentlyTargetedActionMethodFromBecomingExecutedAndInsteadForwardViewModelToView("SomeViewName.cshtml", actionMethodParameterViewModel);
}
You could use an action filter and override the OnActionExecuting event:
public class MyActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
...
}
}
Now let's see what useful information you could extract from this filterContext argument that is passed to this method. The property you should be looking for is called ActionParameters and represents an IDictionary<string, object>. As its name suggests this property contains all the parameters that are passed to the controller action by name and value.
So let's suppose that you have the following controller action:
[MyActionFilter]
public ActionResult Index(MyViewModel model)
{
...
}
Here's how you could retrieve the value of the view model after model binding:
public class MyActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var model = filterContext.ActionParameters["model"] as MyViewModel;
// do something with the model
// You could change some of its properties here
}
}
Now let's see the second part of your question. How to shortcircuit the controller action and redirect to another action?
This could be done by assigning a value to the Result property:
public class MyActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
... some processing here and you decide to redirect:
var routeValues = new RouteValueDictionary(new
{
controller = "somecontroller",
action = "someaction"
});
filterContext.Result = new RedirectToRouteResult(routeValues);
}
}
or for example you decide to shortcircuit the execution of the controller action and directly render a view:
public class MyActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var viewResult = new ViewResult
{
ViewName = "~/Views/FooBar/Baz.cshtml",
};
MyViewModel someModel = ... get the model you want to pass to the view
viewResult.ViewData.Model = model;
filterContext.Result = viewResult;
}
}
or you might decide to render a JSON result:
public class MyActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
MyViewModel someModel = ... get the model you want to pass to the view
filterContext.Result = new JsonResult
{
Data = model,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
}
So as you can see the possibilities are unlimited of what you can do.
I have experimented with the code in the answer provided by the user Darin Dimitrov, and the first and third parts of the answer are correct.
(Though, for others who might find this thread and be interested, I can clarify that in the first answer the "model" does not seem to
be a hardcoded keyword always used for the model but seems to have to correspond to the chosen name of the action method parameter.
In other words, if you instead have the method signature
public ActionResult Index(MyViewModel myViewModel)
then in your action filter you have to use
var model = filterContext.ActionParameters["myViewModel"] as MyViewModel;
)
Regarding the second answer, the usage of 'RedirectToRouteResult' will trigger a new http request (which was not desired as I mentioned in the second code example of mine).
I found another way of "changing" action method by actually invoking it explicitly:
var controller = new SomeController();
ActionResult result = controller.SomeAction(model);
filterContext.Result = result;
The above code actually seems to prevent the originally targeted action method from becoming invoked, i.e. when I put a breakpoint in the method annotated with '[MyActionFilter]' the execution never got into that method.
Typically, it is probably not desired to hardcode a controller like above, but instead reflection might be used, for example as below with the thirdpart library "fasterflect":
string nameOfController = ...
string nameOfActionMethod = ...
// both above variables might for example be derived by using some naming convention and parsing the refering url, depending on what you want to do ...
var theController = this.GetType().Assembly.CreateInstance(nameOfController);
ActionResult result = (ActionResult)theController.CallMethod(nameOfActionMethod, model);
filterContext.Result = result;
(for those who want to extract the names of the current target controller and action method, when implementing logic to determine the controller you want to invoke, you can use this code in the filter:
var routeValueDictionary = filterContext.RouteData.Values;
string nameOfTargetedController = routeValueDictionary["controller"].ToString();
string nameOfTargetedActionMethod = routeValueDictionary["action"].ToString();
)
I think it feels a bit awkward to instantiate and invoke controllers like above, and would prefer to change the target controller and action method in another way if possible ?
So, the remaining question is if there is still (in MVC 4 final version) no way of redirecting/forwarding execution "internally" (without a new http request being fired as with 'RedirectToAction') at the server ?
Basically, I think I am here just looking for something like "Server.Transfer" which was used with ASP.NET Web Forms (and also the old classic ASP I believe could use the same thing).
I have seen older question/answers on this issue with people implementing this behaviour themselves with some "TransferResult" class of their own, but it seems to tend to become broken i different MVC versions.
(for example, see here for MVC 4 beta: How to redirect MVC action without returning 301? (using MVC 4 beta) ).
Is there really still not a simple standard solution (implemented in MVC 4 final) about how to do an "internal redirect" without a new http request (as RedirectToAction does) ?

ASP.NET MVC - Service layer, single or many services in each controller action?

I'm starting to implement a service layer to my MVC project to thin down some bloated controllers (it also has repository / unitofwork pattern).
My question is if you have a complicated view model for a page with lots of child objects etc, and quite a lot of logic going on behind the scenes (to give you an idea the controller the original developer wrote had almost 4000 lines of code!!) is it OK to have multiple services going off doing their thing? or should I just have one big ReportService which does everything?
My controller is starting to look like this? and if I carry on I could end up having quite a lot of different services being called to build up the view model.
Does this look OK or is it starting to go in the wrong direction?
public ViewResult Index(int? reportId)
{
// get the base report object
var reportService = new ReportService();
var report = reportService.GetByReportId(reportId);
var model = Mapper.Map<Report, ReportViewModel>(report);
// get the current active user
var userService = new UserService();
var user = userService.GetCurrentUser();
model.User = Mapper.Map<User, ReportViewModel.UserViewModel>(user);
// get the first unread message
var messageService = new MessageService();
var message = messageService.GetFirstUnread(user.Id);
model.Message = Mapper.Map<Message, ReportViewModel.MessageViewModel>(message);
// get the category navigation
var categoryService = new CategoryService();
var categoryNavigation = categoryService.GetCategoryNavigation(report.Id);
model.CategoryNavigation = Mapper.Map<IEnumerable<Category>, IEnumerable<ReportViewModel.CategoryNavigationViewModel>>(categoryNavigation);
return View(model);
}
It's fine to have multiple small services in your controller. However, there is one thing that's wrong here:
You services should be available through the entire controller and injected through the constructor to achieve loose-coupling.
So something like this:
private readonly IReportService _reportService;
private readonly IUserService _userService;
public SomeConstructor(IReportService reportService, IUserService userService, etc.)
{
_reportService = reportService;
_userService = userService;
// etc
}
That does look like a good approach, an alternative approach would to be split some of this up by using Child Actions - the best solution will depend upon your specific use case though.
If, for example, the ViewModel property CategoryNavigation was being used by the view to create a sort of navigation 'widget' that might be useful in several different Views, you might be better spliting this off into a ChildAction e.g.
[ChildActionOnly]
public ActionResult CategoryNavigationWidget(int reportId)
{
// get the category navigation
var categoryService = new CategoryService();
var categoryNavigation = categoryService.GetCategoryNavigation(report.Id);
return PartialView(categoryNavigation);
}
Any View could then render that ChildAction by going:
#{ Html.RenderAction("CategoryNavigationWidget", "Report",
new { reportId = Model.ReportId }); }
Whether or not this is a good idea will probably depend upon whether or not the 'widget' is reusable.

Asp.net MVC Model for view and Layout

I've been trying to find a good way to handle the Models of our Asp.net MVC websites when having common properties for all the pages. These properties are to be displayed in the Layout (Master Page). I'm using a "BaseModel" class that holds those properties and my Layout use this BaseModel as its model.
Every other model inherits from that BaseModel and each has specific properties relative to the view it represents. As you might have guessed, my Models are actually View Models even if that's not quite relevant here.
I have tried different ways to initialize the BaseModel values
By "hand" in every view
Having a base controller that has an Initialize virtual method to do it (so specific controller can implement specific common behavior for exemple)
Having a base controlelr that override OnActionExecuting to call the Initialize method
Using a helper class to do it outside of the controller
Using a Model Factory
But none of those really appeal to me:
Seems obvious to me, but DRY is one reason enough to justify that (actually I never tried that solution at all, I'm just putting it to be able to loop on that point in the last point).
I don't like that one because it means that whenever a new Controller is added, you need to know that it has to inherit from the BaseController and that you need to call the Initialize method, not to mention that if your controller has overriden the base one, to call the base anyway to maintain the values.
see next point
and 3. are a variation on the same topic but that doesn't really help with the issues of the second solution.
My favorite so far, but now I have to pass a few more variables to set those values. I like it for the inversion of dependence. But then if I want to provide values from the session, I need to pass them explicitly for exemple, then I'm back to square one as I have to provide them by hand (being references or through an interface of any kind)
Of course, (almost) all of those solutions work, but I'm looking for a better way to do it.
While typing this question, I found maybe a new path, the builder pattern that might also do, but implementations can become quickly a burden too, as we can have dozens of views and controllers.
I'll gladly take any serious recommandation/hint/advice/patterns/suggestion !
Update
Thanks to #EBarr I came up with another solution, using an ActionFilterAttribute (not production code, did it in 5 minutes):
public class ModelAttribute : ActionFilterAttribute
{
public Type ModelType { get; private set; }
public ModelAttribute(string typeName) : this(Type.GetType(typeName)) { }
public ModelAttribute(Type modelType)
{
if(modelType == null) { throw new ArgumentNullException("modelType"); }
ModelType = modelType;
if (!typeof(BaseModel).IsAssignableFrom(ModelType))
{
throw new ArgumentException("model type should inherit BaseModel");
}
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var model = ModelFactory.GetModel(ModelType);
var foo = filterContext.RequestContext.HttpContext.Session["foo"] as Foo;
model.Foo = foo;
model.Bar = somevalue;
filterContext.Controller.TempData["model"] = model;
}
}
Calling it is then really simple:
[Model(typeof(HomeModel))]
public ActionResult Index()
{
var homeModel = TempData["model"] as HomeModel;
// Add View Specific stuff
return View(homeModel);
}
And it gives me the best of every world. The only drawback is to find a proper way to passe the model back to the action.
Here it's done using the TempData object, but I also consider updating the model that one can find in the ActionParameters.
I'm still taking any serious recommandation/hint/advice/patterns/suggestion for that, or the previous points.
I went through almost exactly the same process as I dove into MVC. And you're right, none of the solutions feel that great.
In the end I used a series of base models. For various reasons I had a few different types of base models, but the logic should apply to a single base type. The majority of my view models then inherited from one of the bases. Then, depending on need/timing i fill the base portion of the model in ActionExecuting or OnActionExecuted.
A snippet of my code that should make the process clear:
if (filterContext.ActionParameters.ContainsKey("model")) {
var tempModel = (System.Object)filterContext.ActionParameters["model"];
if (typeof(BaseModel_SuperLight).IsAssignableFrom(tempModel.GetType())) {
//do stuff required by light weight model
}
if (typeof(BaseModel_RegularWeight).IsAssignableFrom(tempModel.GetType())) {
//do more costly stuff for regular weight model here
}
}
In the end my pattern didn't feel too satisfying. It was, however, practical, flexible and easy to implement varying levels of inheritance. I was also able to inject pre or post controller execution, which mattered a lot in my case. Hope this helps.
The idea that gave me #EBarr to use an action filter was actually working but felt wrong in the end, because there was no clean way to retrieve the model without passing through a viewbag, or the httpcontext items, or something alike. Also, it made mandatory to decorate every action with its model. It also made the postback more difficult to handle. I still believe that this solution has merits and might be useful in some specific scenarios.
So I was back to square one and started looking more into that topic. I came to the following. First the problem has two aspects
Initializing the data for the views
Rendering the data
While looking for more idea, I realized that I was not looking at the problem from the right perspective. I was looking at it from a "Controller" POV, whereas the final client for the model is the view. I was also reminded that the Layout/Master page is not a view and should not have a model associated with it. That insight put me on what feels the right path for me. Because it meant that every "dynamic" part of the Layout should be handled outside of it. Of course, sections seems the perfect fit for that, because of their flexibility.
On the test solution I made, I had (only) 4 different sections, some mandatory, some not. The problem with sections, is that you need to add them on every page, which can quickly be a pain to update/modify. To solve that, I tried this:
public interface IViewModel
{
KeyValuePair<string, PartialViewData>[] Sections { get; }
}
public class PartialViewData
{
public string PartialViewName { get; set; }
public object PartialViewModel { get; set; }
public ViewDataDictionary ViewData { get; set; }
}
For exemple, my model for the view is this:
public class HomeViewModel : IViewModel
{
public Article[] Articles { get; set; } // Article is just a dummy class
public string QuickContactMessage { get; set; } // just here to try things
public HomeViewModel() { Articles = new Article[0]; }
private Dictionary<string, PartialViewData> _Sections = new Dictionary<string, PartialViewData>();
public KeyValuePair<string, PartialViewData>[] Sections
{
get { return _Sections.ToArray(); }
set { _Sections = value.ToDictionary(item => item.Key, item => item.Value); }
}
}
This get initialized in the action:
public ActionResult Index()
{
var hvm = ModelFactory.Get<HomeViewModel>(); // Does not much, basicaly a new HomeViewModel();
hvm.Sections = LayoutHelper.GetCommonSections().ToArray(); // more on this just after
hvm.Articles = ArticlesProvider.GetArticles(); // ArticlesProvider could support DI
return View(hvm);
}
LayoutHelper is a property on the controller (which could be DI'ed if needed):
public class DefaultLayoutHelper
{
private Controller Controller;
public DefaultLayoutHelper(Controller controller) { Controller = controller; }
public Dictionary<string, PartialViewData> GetCommonSections(QuickContactModel quickContactModel = null)
{
var sections = new Dictionary<string, PartialViewData>();
// those calls were made in methods in the solution, I removed it to reduce the length of the answer
sections.Add("header",
Controller.UserLoggedIn() // simple extension that check if there is a user logged in
? new PartialViewData { PartialViewName = "HeaderLoggedIn", PartialViewModel = new HeaderLoggedInViewModel { Username = "Bishop" } }
: new PartialViewData { PartialViewName = "HeaderNotLoggedIn", PartialViewModel = new HeaderLoggedOutViewModel() });
sections.Add("quotes", new PartialViewData { PartialViewName = "Quotes" });
sections.Add("quickcontact", new PartialViewData { PartialViewName = "QuickContactForm", PartialViewModel = model ?? new QuickContactModel() });
return sections;
}
}
And in the views (.cshtml):
#section quotes { #{ Html.RenderPartial(Model.Sections.FirstOrDefault(s => s.Key == "quotes").Value); } }
#section login { #{ Html.RenderPartial(Model.Sections.FirstOrDefault(s => s.Key == "header").Value); } }
#section footer { #{ Html.RenderPartial(Model.Sections.FirstOrDefault(s => s.Key == "footer").Value); } }
The actual solution has more code, I tried to simplify to just get the idea here. It's still a bit raw and need polishing/error handling, but with that I can define in my action, what the sections will be, what model they will use and so on. It can be easily tested and setting up DI should not be an issue.
I still have to duplicate the #section lines in every view, which seems a bit painful (especialy because we can't put the sections in a partial view).
I'm looking into the templated razor delegates to see if that could not replace the sections.

Determine the model of a partial view from the controller within MVC

My current problem is that I have a partial view that I want to determine what model is being used by it.
I have had to deal with a few strange scenarios for my project so I will try to outline it here, maybe someone can offer a better way to do this.
I am designing something like the Google iGoogle page. A main page with multiple widgets that are able to move around or be configured as needed. The current system loads the actual widget's data asynchronously view a POST to a controller within my application. That controller will either render a partial view to HTML that can be returned (and then loaded into the page view JQUERY) or just straight HTML/JavaScript that is stored in a database.
This was working fine for me, I had a model for the widgets that holds a dictionary of options that are described via the database, and then used by the partial view. The problem came when I wanted to pass data to a partial view. The best solution I could come up with was having the controller determine which model the partial view in question uses, have some function that will fill the model, and then pass it, along with the partial view, to the function that will render it to HTML within the controller.
I realize this is an odd scenario for MVC (the layers are blending...) and any advice on fundamental design, or implementation of this would be greatly appreciated.
I am currently using MVC3/Razor. Feel free to ask any other questions.
I prototyped a possible solution to this, because it seemed like a fun problem. I hope it's useful to you.
Models
First, the models. I decided to create two 'widgets', one for news, and one for a clock.
public class NewsModel
{
public string[] Headlines { get; set; }
public NewsModel(params string[] headlines)
{
Headlines = headlines;
}
}
public class ClockModel
{
public DateTime Now { get; set; }
public ClockModel(DateTime now)
{
Now = now;
}
}
Controller
My controller doesn't know anything about the views. What it does is returns a single model, but that model has the ability to dynamically fetch the right model as required by the view.
public ActionResult Show(string widgetName)
{
var selector = new ModelSelector();
selector.WhenRendering<ClockModel>(() => new ClockModel(DateTime.Now));
selector.WhenRendering<NewsModel>(() => new NewsModel("Headline 1", "Headline 2", "Headline 3"));
return PartialView(widgetName, selector);
}
Delegates are used so that the correct model is only created/fetched if it is actually used.
ModelSelector
The ModelSelector that the controller uses is pretty simple - it just keeps a bag of delegates to create each model type:
public class ModelSelector
{
private readonly Dictionary<Type, Func<object>> modelLookup = new Dictionary<Type, Func<object>>();
public void WhenRendering<T>(Func<object> getter)
{
modelLookup.Add(typeof(T), getter);
}
public object GetModel(Type modelType)
{
if (!modelLookup.ContainsKey(modelType))
{
throw new KeyNotFoundException(string.Format("A provider for the model type '{0}' was not provided", modelType.FullName));
}
return modelLookup[modelType]();
}
}
The Views - Simple solution
Now, the easiest way to implement a view would be:
#model MvcApplication2.ModelSelector
#using MvcApplication2.Models
#{
var clock = (ClockModel) Model.GetModel(typeof (ClockModel));
}
<h2>The time is: #clock.Now</h2>
You could end here and use this approach.
The Views - Better solution
That's pretty ugly. I wanted my views to look like this:
#model MvcApplication2.Models.ClockModel
<h2>Clock</h2>
#Model.Now
And
#model MvcApplication2.Models.NewsModel
<h2>News Widget</h2>
#foreach (var headline in Model.Headlines)
{
<h3>#headline</h3>
}
To make this work, I had to create a custom view engine.
Custom view engine
When a Razor view is compiled, it inherits a ViewPage<T>, where T is the #model. So we can use reflection to figure out what type the view wanted, and select it.
public class ModelSelectorEnabledRazorViewEngine : RazorViewEngine
{
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var result = base.CreateView(controllerContext, viewPath, masterPath);
if (result == null)
return null;
return new CustomRazorView((RazorView) result);
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var result = base.CreatePartialView(controllerContext, partialPath);
if (result == null)
return null;
return new CustomRazorView((RazorView)result);
}
public class CustomRazorView : IView
{
private readonly RazorView view;
public CustomRazorView(RazorView view)
{
this.view = view;
}
public void Render(ViewContext viewContext, TextWriter writer)
{
var modelSelector = viewContext.ViewData.Model as ModelSelector;
if (modelSelector == null)
{
// This is not a widget, so fall back to stock-standard MVC/Razor rendering
view.Render(viewContext, writer);
return;
}
// We need to work out what #model is on the view, so that we can pass the correct model to it.
// We can do this by using reflection over the compiled views, since Razor views implement a
// ViewPage<T>, where T is the #model value.
var compiledViewType = BuildManager.GetCompiledType(view.ViewPath);
var baseType = compiledViewType.BaseType;
if (baseType == null || !baseType.IsGenericType)
{
throw new Exception(string.Format("When the view '{0}' was compiled, the resulting type was '{1}', with base type '{2}'. I expected a base type with a single generic argument; I don't know how to handle this type.", view.ViewPath, compiledViewType, baseType));
}
// This will be the value of #model
var modelType = baseType.GetGenericArguments()[0];
if (modelType == typeof(object))
{
// When no #model is set, the result is a ViewPage<object>
throw new Exception(string.Format("The view '{0}' needs to include the #model directive to specify the model type. Did you forget to include an #model line?", view.ViewPath));
}
var model = modelSelector.GetModel(modelType);
// Switch the current model from the ModelSelector to the value of #model
viewContext.ViewData.Model = model;
view.Render(viewContext, writer);
}
}
}
The view engine is registered by putting this in Global.asax.cs:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ModelSelectorEnabledRazorViewEngine());
Rendering
My home view includes the following lines to test it all out:
#Html.Action("Show", "Widget", new { widgetName = "Clock" })
#Html.Action("Show", "Widget", new { widgetName = "News" })
One option would be to extend the idea of partial requests in your application. Steve Sanderson has a fantastic example of this, although the post relates to MVC 1 & 2. I think it would still help in you v3, but I haven't investigated v3 to see if the MVC team implemented their own version. In your asynch scenario, you'll need to toy with the implementation a bit, perhaps change the PartialRequest definition to accept different information as needed, but I think this might be a good start. The net result would be better isolation of concerns, allowing individual controllers to manage a particular type of partial, and in turn be better aware of the model Type you want to work with.
I'm not 100% sure that this is what you'd be looking for, but the [ChildActionOnly] attribute can be added to a method within your controller. That requires that the method can only be called from a partial view. Then you can set up your partial view for that method that basically resembles one of your widgets. Check out the MVC Music Store example here:
http://www.asp.net/mvc/tutorials/mvc-music-store-part-10
What about a dynamic view model? Layouts in MVC3 use them, and maybe you can use something similar for your purposes:
Dynamic in C# 4.0: Introducing the ExpandoObject
Fun With Method Missing and C# 4
Dynamic View Page, MVC without a View Model
I blogged about doing exactly this. Please see http://blogs.planetcloud.co.uk/mygreatdiscovery/?tag=/widget
Essentially I built out a similar widget system. The posts also cover how to handle configuration of those widgets. This makes use of the dynamic support in Mvc3 so that any model object can be passed to the view, from a single controller action.
By default all widgets have a collection of KVP properties (I believe this is what the OP has). So for a simple widget we get access to those properties from within the view. I used for a widget that displayed some html (where the html was stored in one of those properties).
However, for more complex widgets we implement IWidgetWithDisplayModel. This tells us that before we pass the loaded widget back to the view, we need to "build" our display model.
Here's the controller action that does that. Check the posts for full details.
[HttpGet]
public ActionResult Get(string name)
{
var widget = widgetService.GetWidgetBySystemName(name, true);
if (widget == null)
return Content(string.Format("Widget [{0}] not found!", name));
if (!this.ViewExists(widget.WidgetName))
return Content(string.Format("A template for widget [{0}] was not found.", widget.WidgetName));
if (widget is IWidgetWithDisplayModel) {
(widget as IWidgetWithDisplayModel).CreateDisplayModel();
}
return PartialView(widget.WidgetName, widget);
}

MasterLocationFormats in WebFormViewEngine not used?

I tried to make the ViewEngine use an additional path using:
base.MasterLocationFormats = new string[] {
"~/Views/AddedMaster.Master"
};
in the constructor of the ViewEngine. It works well for aspx and ascx(PartialViewLocationFormats, ViewLocationFormats).
I still have to supply the MasterPage in web.config or in the page declaration. But if I do, then this declaration is used, not the one in the ViewEngine.
If I use am empty MasterLocationFormats, no error is thrown. Is this not implemeted in RC1?
EDIT:
using:
return View("Index", "AddedMaster");
instead of
return View("Index");
in the Controller worked.
Your example isn't really complete, but I am going to guess that your block of code exists at the class level and not inside of a constructor method. The problem with that is that the base class (WebFormViewEngine) initializes the "location format" properties in a constructor, hence overriding your declaration;
public CustomViewEngine()
{
MasterLocationFormats = new string[] {
"~/Views/AddedMaster.Master"
};
}
If you want the hard-coded master to only kick in as a sort of last effort default, you can do something like this:
public CustomViewEngine()
{
MasterLocationFormats = new List<string>(MasterLocationFormats) {
"~/Views/AddedMaster.Master"
}.ToArray();
}

Resources