Related
In my ASP.Net Core MVC 6 solution I have two sets of controllers. One set contains the webpages with their regular views. Another set contains the API controllers.
To avoid duplicating db logic the web controllers are using the API controllers. Currently I am creating an instance of the required controller manually by handing it a DbContext as constructor argument. This is the DbContext given to web controller by dependency injection.
But whenever I add another constructor parameter to the API controller I need to modify all web controllers that use this API controller.
How can I use the dependency injection system builtin to ASP.Net 5 to create an instance of the required API controller for me? Then it would fill in the required constructor parameters automatically.
One solution could be to move the db logic from the API controllers to a separate layer and call that from both API and web controllers. This would not solve my problem since the new layer would still need the same parameters and I'm not fan of the unnecessary wiring.
Another solution would be to have the web controllers access the API through a web call, but that just adds complexity to the app.
Today I am doing this:
public IActionResult Index()
{
using (var foobarController = new Areas.Api.Controllers.FoobarController(
// All of these has to be in the constructor of this controller so they can be passed on to the ctor of api controller
_dbContext, _appEnvironment,
_userManager, _roleManager,
_emailSender, _smsSender))
{
var model = new IndexViewModel();
model.Foo = foobarController.List(new FoobarRequest() { Foo = true, Bar = false });
model.Bar = foobarController.List(new FoobarRequest() { Foo = false, Bar = true });
return View(model);
}
}
And I am hoping for something like this:
(This example does not work.)
using (var foobarController = CallContextServiceLocator.Locator.ServiceProvider.GetService<Areas.Api.Controllers.FoobarController>())
{
var model = new IndexViewModel();
model.Foo = foobarController.List(new FoobarRequest() { Foo = true, Bar = false });
model.Bar = foobarController.List(new FoobarRequest() { Foo = false, Bar = true });
return View(model);
}
How can I use the dependency injection system builtin to ASP.Net 5 to create an instance of the required API controller for me?
In your Startup.cs can tell the MVC to register all your controllers as services.
services.AddMvc().AddControllersAsServices();
Then you can simply inject the desired controller in your other controller via the DI mechanism and invoke its action method.
Don't do it. Move that logic to another component that gets shared between the 2 controllers. The controller is dispatched to by the framework as a result of an HTTP call, its not your public API surface. In general, your controllers should be used as a the place where the HTTP request is transformed into business objects. Operations on those objects should be delegate to another layer (especially if it needs to be used from more than one place in your application).
To be able to use a controller from another controller you need to:
Register the controller in Startup.cs ConfigureServices: services.AddTransient <Areas.Api.Controllers.FoobarController, Areas.Api.Controllers.FoobarController>();
You must pass the controller you want to access as a ctor parameter into the main controller.
If you need to access local properties in the controller such as User or Url there are two ways to do this.
The first way is to use DI to get an instance of IHttpContextAccessor to access User and IUrlHelper to access Url objects:
public class FoobarController : Controller
{
private readonly ApplicationDbContext _dbContext;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUrlHelper _urlHelper;
public FoobarController(ApplicationDbContext dbContext, IHttpContextAccessor httpContextAccessor, IUrlHelper _urlHelper, [...])
{
_dbContext = dbContext;
_httpContextAccessor = httpContextAccessor;
_urlHelper = urlHelper;
}
public FoobarResponse List(FoobarRequest request)
{
var userId = _httpContextAccessor.HttpContext.User.GetUserId();
var response = new FoobarResponse();
response.List = _dbContext.Foobars.Where(f => f.UserId == userId).ToList();
response.Thumb =
return response;
}
}
The second way is to set it in the calling controller:
public class HomeController : Controller
{
private Areas.Api.Controllers.FoobarController _foobarController;
public HomeController(Areas.Api.Controllers.FoobarController foobarController)
{
_foobarController = foobarController;
}
private void InitControllers()
{
// We can't set this at Ctor because we don't have our local copy yet
// Access to Url
_foobarController.Url = Url;
// Access to User
_foobarController.ActionContext = ActionContext;
// For more references see https://github.com/aspnet/Mvc/blob/6.0.0-rc1/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs
// Note: This will change in RC2
}
public IActionResult Index()
{
InitControllers();
var model = new IndexViewModel();
model.Foo = _foobarController.List(new FoobarRequest() { Foo = true, Bar = false });
model.Bar = _foobarController.List(new FoobarRequest() { Foo = false, Bar = true });
return View(model);
}
}
The source code for ASP.Net Core MVC6 RC1 Controller can be found here. It is however undergoing heavy rewrite for RC2 and with it the properties that has to be copied to get access to User and Url will change.
#B12Toaster is correct for MVC but if you only use ApiController you should do it like this:
services.AddControllers().AddControllersAsServices();
Why would your new layer need wiring up? Why not take in an object into both controllers and call a method on that object. The DI container could resolve the dependencies of this new object without duplicated wiring couldn't it?
ie you could have this:
public class MvcController
{
SharedComponent sharedComponent;
public MvcController(SharedComponent sharedComponent)
{
this.sharedComponent = sharedComponent;
}
public IActionResult Index()
{
var model = new IndexViewModel();
model.Foo = shredComponent.List(new FoobarRequest() { Foo = true, Bar = false });
model.Bar = shredComponent.List(new FoobarRequest() { Foo = false, Bar = true });
return View(model);
}
}
//Repeat this for the API controller
public class SharedComponent
{
public SharedComponent(DBContext dbContext, AppEnvironment appEnvironment, UserManager userManager, RoleManager roleManager,
EmailSender emailSender, SmsSender smsSender)
{
...Store in fields for later usage
}
}
I'd have to agree with others that injecting the controller may not be the best route. Mostly because it marries the business logic with ASP.Net instead of treating it like an IO device like, in my opinion, it should be.
Let's say we have an interface that looks like this:
public interface ICalculator {
int Add(int left, int right);
}
and we have an implementation that stores the business logic:
public class MyCalculator : ICalculator {
public int Add(int left, int right) => left + right;
}
This implementation can be used as a background service, within the same process as a WPF application, or as an ASP.NET WebAPI controller. It would look something like this:
[ApiController]
[Route("api/{controller}")]
public void CalculatorController : Controller, ICalculator {
private readonly ICalculator _calculator;
public CalculatorController(ICalculator calc) => _calculator = calc;
[Route("Add")]
public int Add(int left, int right) => _calculator.Add(left, right);
}
If that controller has a dependency on a repository you can inject that interface too. Personally I like defining a collection of repositories (like IUserRepository for example) and injecting only what is needed instead of the entire DbContext.
public CalculatorController(ICalculator calculator, IDbContext db) { }
There's nothing wrong with a controller depending on more than just the thing it is decorating. Just make sure you have a set of tests that assert various things. For example you could assert that when a particular controller method is called the particular method on the other interface is also called.
Personally I find this approach a better fit. It's okay to use certain technologies but they should be kept at arm's length from the business rules. A developer should be able to take the business rules that govern a particular part of the code and switch from a WCF service to ASP.NET WebAPI trivially.
I've personally been a part of a couple projects where we had to switch from one database technology to another (SQL Server to CouchDB) and one where our micro-services needed to be running as restful Web API services instead of Windows services. If you architect things this way those types of projects become relatively trivial compared to how things are normally composed.
I have this really basic code in a MVC controller action. It maps an Operation model class to a very basic OperationVM view-model class .
public class OperationVM: Operation
{
public CategoryVM CategoryVM { get; set; }
}
I need to load the complete list of categories in order to create a CategoryVM instance.
Here's how I (try to) create a List<OperationVM> to show in the view.
public class OperationsController : Controller {
private SomeContext context = new SomeContext ();
public ViewResult Index()
{
var ops = context.Operations.Include("blah...").ToList();
Mapper.CreateMap<Operation, OperationVM>()
.ForMember(
dest => dest.CategoryVM,
opt => opt.MapFrom(
src => CreateCatVM(src.Category, context.Categories)
// trouble here ----------------^^^^^^^
)
);
var opVMs = ops.Select(op => Mapper.Map<Operation, OperationVM>(op))
.ToList();
return View(opVMs);
}
}
All works great first time I hit the page. The problem is, the mapper object is static. So when calling Mapper.CreateMap(), the instance of the current DbContext is saved in the closure given to CreateMap().
The 2nd time I hit the page, the static map is already in place, still using the reference to the initial, now disposed, DbContext.
The exact error is:
The operation cannot be completed because the DbContext has been disposed.
The question is: How can I make AutoMapper always use the current context instead of the initial one?
Is there a way to use an "instance" of automapper instead of the static Mapper class?
If this is possible, is it recommended to re-create the mapping every time? I'm worried about reflection slow-downs.
I read a bit about custom resolvers, but I get a similar problem - How do I get the custom resolver to use the current context?
It is possible, but the setup is a bit complicated. I use this in my projects with help of Ninject for dependency injection.
AutoMapper has concept of TypeConverters. Converters provide a way to implement complex operations required to convert certain types in a separate class. If converting Category to CategoryVM requires a database lookup you can implement that logic in custom TypeConverter class similar to this:
using System;
using AutoMapper;
public class CategoryToCategoryVMConverter :
TypeConverter<Category, CategoryVM>
{
public CategoryToCategoryVMConverter(DbContext context)
{
this.Context = context;
}
private DbContext Context { get; set; }
protected override CategoryVM ConvertCore(Category source)
{
// use this.Context to lookup whatever you need
return CreateCatVM(source, this.Context.Categories);
}
}
You then to configure AutoMapper to use your converter:
Mapper.CreateMap<Category, CategoryVM>().ConvertUsing<CategoryToCategoryVMConverter>();
Here comes the tricky part. AutoMapper will need to create a new instance of our converter every time you map values, and it will need to provide DbContext instance for constructor. In my projects I use Ninject for dependency injection, and it is configured to use the same instance of DbContext while processing a request. This way the same instance of DbContext is injected both in your controller and in your AutoMapper converter. The trivial Ninject configuration would look like this:
Bind<DbContext>().To<SomeContext>().InRequestScope();
You can of course use some sort of factory pattern to get instance of DbContext instead of injecting it in constructors.
Let me know if you have any questions.
I've found a workaround that's not completely hacky.
Basically, I tell AutoMapper to ignore the tricky field and I update it myself.
The updated controller looks like this:
public class OperationsController : Controller {
private SomeContext context = new SomeContext ();
public ViewResult Index()
{
var ops = context.Operations.Include("blah...").ToList();
Mapper.CreateMap<Operation, OperationVM>()
.ForMember(dest => dest.CategoryVM, opt => opt.Ignore());
var opVMs = ops.Select(
op => {
var opVM = Mapper.Map<Operation, OperationVM>(op);
opVM.CategoryVM = CreateCatVM(op.Category, context.Categories);
return opVM;
})
.ToList();
return View(opVMs);
}
}
Still curious how this could be done from within AutoMapper...
The answer from #LeffeBrune is perfect. However, I want to have the same behavior, but I don't want to map every property myself. Basically I just wanted to override the "ConstructUsing".
Here is what I came up with.
public static class AutoMapperExtension
{
public static void ConstructUsingService<TSource, TDestination>(this IMappingExpression<TSource, TDestination> mappingExression, Type typeConverterType)
{
mappingExression.ConstructUsing((ResolutionContext ctx) =>
{
var constructor = (IConstructorWithService<TSource, TDestination>)ctx.Options.ServiceCtor.Invoke(typeConverterType);
return constructor.Construct((TSource)ctx.SourceValue);
});
}
}
public class CategoryToCategoryVMConstructor : IConstructorWithService<Category, CategoryVM>
{
private DbContext dbContext;
public DTOSiteToHBTISiteConverter(DbContext dbContext)
{
this.dbContext = dbContext;
}
public CategoryVM Construct(Category category)
{
// Some commands here
if (category.Id > 0)
{
var vmCategory = dbContext.Categories.FirstOrDefault(m => m.Id == category.Id);
if (vmCategory == null)
{
throw new NotAllowedException();
}
return vmCategory;
}
return new CategoryVM();
}
}
// Initialization
Mapper.Initialize(cfg =>
{
cfg.ConstructServicesUsing(type => nInjectKernelForInstance.Get(type));
cfg.CreateMap<Category, CategoryVM>().ConstructUsingService(typeof(CategoryToCategoryVMConstructor));
};
Our ASP.NET MVC application allows an authenticated user to administer one or more "sites" linked to their account.
Our Urls are highly guessible since we use the site friendly name in the URL rather than the Id e.g:
/sites/mysite/
/sites/mysite/settings
/sites/mysite/blog/posts
/sites/mysite/pages/create
As you can see we need access to the site name in a number of routes.
We need to execute the same behaviour for all of these actions:
Look for a site with the given identifier on the current account
If the site returned is null, return a 404 (or custom view)
If the site is NOT null (valid) we can carry on executing the action
The current account is always available to us via an ISiteContext object. Here is how I might achieve all of the above using a normal route parameter and performing the query directly within my action:
private readonly ISiteContext siteContext;
private readonly IRepository<Site> siteRepository;
public SitesController(ISiteContext siteContext, IRepository<Site> siteRepository)
{
this.siteContext = siteContext;
this.siteRepository = siteRepository;
}
[HttpGet]
public ActionResult Details(string id)
{
var site =
siteRepository.Get(
s => s.Account == siteContext.Account && s.SystemName == id
);
if (site == null)
return HttpNotFound();
return Content("Viewing details for site " + site.Name);
}
This isn't too bad, but I'm going to need to do this on 20 or so action methods so want to keep things as DRY as possible.
I haven't done much with custom model binders so I wonder if this is a job better suited for them. A key requirement is that I can inject my dependencies into the model binder (for ISiteContext and IRepository - I can fall back to DependencyResolver if necessary).
Many thanks,
Ben
Update
Below is the working code, using both a custom model binder and action filter. I'm still not sure how I feel about this because
Should I be hitting my database from a modelbinder
I can actually do both the retrieving of the object and null validation from within an action filter. Which is better?
Model Binder:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (!controllerContext.RouteData.Values.ContainsKey("siteid"))
return null;
var siteId = controllerContext.RouteData.GetRequiredString("siteid");
var site =
siteRepository.Get(
s => s.Account == siteContext.Account && s.SystemName == siteId
);
return site;
}
Action Filter:
public class ValidateSiteAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var site = filterContext.ActionParameters["site"];
if (site == null || site.GetType() != typeof(Site))
filterContext.Result = new HttpNotFoundResult();
base.OnActionExecuting(filterContext);
}
}
Controller Actions:
[HttpGet]
[ValidateSite]
public ActionResult Settings(Site site)
{
var blog = site.GetFeature<BlogFeature>();
var settings = settingsProvider.GetSettings<BlogSettings>(blog.Id);
return View(settings);
}
[HttpPost]
[ValidateSite]
[UnitOfWork]
public ActionResult Settings(Site site, BlogSettings settings)
{
if (ModelState.IsValid)
{
var blog = site.GetFeature<BlogFeature>();
settingsProvider.SaveSettings(settings, blog.Id);
return RedirectToAction("Settings");
}
return View(settings);
}
This definitely sounds like a job for an action filter. You can do DI with action filters not a problem.
So yeah, just turn your existing functionality into a action filter and then apply that to each action OR controller OR a base controller that you inherit from.
I don't quite know how your site works but you could possibly use a global action filter that checks for the existence of a particular route value, e.g. 'SiteName'. If that route value exists, that means you need to follow through with checking that the site exists...
A custom model binder for your Site type sounds like a good idea to me.
You will probably also want an action filter as well to catch "null" and return not found.
I am new in unit test and MVC development.
I have a question for using moq for unit testing in asp.net mvc. I have a controller which accepts an ajax action:
[HttpPost,Authorize]
public ActionResult GrabLink()
{
string username = HttpContext.User.Identity.Name;
string rssUrl = Request.Params["Grablink"].ToString();
...}
This action deals with the http request which I generate from the view:
var mockRequest = new Moq.Mock<HttpRequestBase>();
but I can not find a way to define the parameters I used in the class. Also, is there any way to use the value binding provider directly to pass the value to the controller if I would like to do an ajax post?
I am a newbie in handling web request. If you have some good tutorial for better understanding the Http request (as well as the Httpcontext and related classes in asp.net) please post here. Thank you very much!
This works very well for me:
var controller = new HomeController();
var context = new Mock<HttpContextBase>(MockBehavior.Strict);
var controllerContext = new Mock<ControllerContext>();
controllerContext.SetupGet(x => x.HttpContext.User.Identity.Name)
.Returns("TestUser");
controllerContext.SetupGet(x => x.HttpContext.User.Identity.IsAuthenticated)
.Returns(true);
controllerContext.SetupGet(x => x.HttpContext.Request.IsAuthenticated)
.Returns(true);
controller.ControllerContext = controllerContext.Object;
// As a bonus, instantiate the Url helper to allow creating links
controller.Url = new UrlHelper(
new RequestContext(context.Object, new RouteData()), new RouteCollection());
This will allow you to initialize any user you want as an authenticated user, and the last line will allow you to user the Url helper within the controller even though you're calling it from a unit test.
As Scott said HttpContext makes Controllers hard to test. Anyway he's got a pretty solution at here.
BTW why didn't you make rssUrl a parameter if it is assigning by POST or GET?
e.g.
//POST: /GrabLink?rssUrl=bla bla...
[HttpPost,Authorize]
public ActionResult GrabLink(IPrincipal user, string rssUrl) {
string userName = user.Name;
}
Ok, #cem covered your second question very well.
For your first, nerddinner, and If I'm not mistaken, when you create a new Internet Application with Unit test, in Visual Studio, have the following mock classes for HttpContext. Its at the bottom of this file.
You could use these (or create a new Internet App +Tests with VS) and copy all the fake classes for your tests. (I wrote a Moq example below)
It looks like this:
public class MockHttpContext : HttpContextBase {
private IPrincipal _user;
public override IPrincipal User {
get {
if (_user == null) {
_user = new MockPrincipal();
}
return _user;
}
set {
_user = value;
}
}
public override HttpResponseBase Response
{
get
{
return new MockHttpResponse();
}
}
}
public class MockHttpResponse : HttpResponseBase {
public override HttpCookieCollection Cookies
{
get
{
return new HttpCookieCollection();
}
}
}
Not tested, but to Use mock it would look like this:
var fakeReqBase = new Mock<HttpRequestBase>();
fakeReqBase.Setup(f => f.User).Returns(new GenericIdentity("FakeUser"));
//generic identity implements IIdentity
fakeUserRepo.Object;//this returns fake object of type HttpRequestBase
Checkout the Moq Quickstart. Its quite easy to get used to, and the fluent interface really helps.
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);
}