A new MVC 5 app that I'm working on references a large-ish collection (4 assemblies, about 500 classes each) of data models generated from a 4GL environment. The basic interaction has the MVC app present and populate a model instance, then (after model validation), hand the model instance off to a provider for processing.
The initial approach I've used is, for each model, create
a scaffolded razor view bound to the model,
a partial controller class with a pair of actions (GET/POST) the model
All of the actions are part of the same controller class which has a couple of private methods to implement the GET & POST actions exposed in each of the partials.
So, the structure is like:
|
|--\Controllers
|
|--MyController.cs
|--MyController.MDL001.cs
|--MyController.MDL002.cs
|-- ...
|--MyController.MDL500.cs
|--\Views
|
|--\My
|--\MDL001.cshtml
|--\MDL002.cshtml
|-- ...
|--\MDL500.cshtml
And the implementation of each partial controller follows the pattern:
public partial class MyController
{
public ActionResult ProcessMDL001(MDL001Model modelInstance)
{
return ProcessModel(modelInstance);
}
public ActionResult MDL001()
{
return ShowModel("MDL001");
}
}
Where methods ProcessModel(...) and ShowModel(...) are defined in MyController.cs
I want to keep MVC's model binding and validation functioning but also am keen on avoiding a few thousand nearly-identical concrete action implementations. Is there some pattern/approach using routing, generics, dynamics, etc. that can help here?
Assuming that you can roughly treat each class the same, you can handle this with generics:
public class BaseController<T> : Controller
where T : class, new
{
public ActionResult Process(T modelInstance)
{
return ProcessModel(modelInstance);
}
...
}
But, you would need to use subclasses instead of partial classes. Essentially, you're just going to implement the action once, and then subclass this base controller to specify the type for the controller instance you're working with:
public MDL001ModelController : BaseController<MDL001Model>
{
}
If no additional type-specific actions are needed, then that code alone is all you subclass would need to be. However, you can always add additional actions that will only apply to this particular controller instance.
If there are pieces of the common actions that you need to customize slightly, such as validation logic or something, you can provide hooks in your actions. Something along the lines of:
public class BaseController<T> : Controller
where T : class, new
{
public ActionResult ActionWithHook(T model)
{
DoSomeWork(model);
return View();
}
// By default this does nothing, but can be overridden to do something
internal virtual void DoSomeWork(T model)
{
}
}
Then:
public MDL001ModelController : BaseController<MDL001Model>
{
internal override void DoSomeWork(MDL001Model model)
{
// Do something
}
}
Related
For Asp.net mvc core app:
In one of my dbcontext tables I have records with HelpInfo (id as primary key, and string for helpInfo) that I want to use as html title attributes in my razor views, such as:
<div title='#I(12)'></div>
Where 12 is an example id of a HelpInfo record and #I should be a global accessible class method that fetches the corresponding helpInfo string.
I tried to implement King Kings approach such as:
The CustomView:
public abstract class CustomView<TModel> : RazorPage<TModel>
{
protected IInfo Info => Context.RequestServices.GetRequiredService<IInfo>();
public Task<string> I(int code)
{
return Info.GetAsync(code);
}
}
The Interface:
public interface IInfo
{
Task<string> GetAsync(int code);
}
The Service:
public class Info : IInfo
{
private readonly ApplicationDbContext context;
public Info(ApplicationDbContext context)
{
this.context = context;
}
public async Task<string> GetAsync(int code)
{
var myRecord = await context.MyRecords.FindAsync(code);
if (myRecord != null)
{
return myRecord.Info;
}
return null;
}
}
In _ViewImports.cshtml I added #inherits CustomView<TModel>
In the view I used <div title='#(await I(12))'>Test</div>
When I load the view I get
No service for type '...Models.IInfo' has been registered
Any help to pin down the problem would be appreciated.
As I understand looks like you want something like #Html or #Url which are supported in the default Page and RazorPage. That's just a custom property exposed by the base page. So in your case, you need a custom view (for mvc) or a custom page (for razor pages). Here is an example of a custom view used for MVC:
public abstract class CustomView<TModel> : RazorPage<TModel>
{
//custom members can be declared in here
}
Based on your desired usage of I, it must be a method. So it can be a method exposed by some service injected in your base page. Suppose that interface is like this:
public interface IInfo {
string Get(int code);
}
Now your custom page can implement the I method like this:
public abstract class CustomView<TModel> : RazorPage<TModel>
{
//NOTE: the custom page class does not support constructor injection
//So we need to get the injected services via the Context like this.
protected IInfo Info => Context.RequestServices.GetRequiredService<IInfo>();
public string I(int code){
return Info.Get(code);
}
}
To use your custom view, you need to use the directive #inherits in your view or better in the _ViewImports.cshtml so that you don't have to repeat that #inherits everywhere, like this:
#inherits CustomView<TModel>
Sometimes the base view is not applied (so the base members are not available) until you rebuild your project.
Now you can use the I in your views as what you desire, like this:
<div title="#I(12)"></div>
Note the I method returns a string in my example, you can also make it return an IHtmlContent (e.g: HtmlString) or whatever type you need for your requirement.
NOTE:
Technically you can use any services (including ones querying for data from database), but in such cases please ensure that the querying is as fast as possible and use async to have the best performance & avoid thread starvation. Here is an example of the IInfo service that queries from a database:
public interface IInfo {
Task<string> GetAsync(int code);
}
public class Info : IInfo {
//inject whatever your Info needs
//here we just need some DbContext (used in EFCore)
public Info(InfoDbContext dbContext){
_infoDbContext = dbContext;
}
readonly InfoDbContext _infoDbContext;
public async Task<string> GetAsync(int code){
var info = await _infoDbContext....
return info;
}
}
The I method then should be async as well:
public Task<string> I(int code){
return Info.GetAsync(code);
}
I hope that you know how to register a DbContext to use in your code (that's part of EFCore so you may have to learn more about that first). Now to use the async method, you call it in your view like this:
<div title="#(await I(12))"></div>
Again, I would try to avoid querying the db in such a helper like that. As I said, the helper's method can be called multiple times right in one same view so usually we have fast methods or use caching for the info it need. This is related to another feature called caching which has more to be put in one short answer, you can learn more about that.
Solution:
To my Startup.cs I added:
services.AddScoped<IInfo, Info>();
services.AddTransient<IInfo, Info>(); also does work.
I have created ASP.NET Core application and now I'm trying to use ViewBag in _LoginPartial. I have created base controller:
public class BaseController : Controller
{
public ApplicationDbContext _db;
public BaseController(ApplicationDbContext db)
{
_db = db;
ViewData["MyKey"] = _db.MyTable.ToList();
}
}
All my controllers derive from this BaseController.
But when I see ViewData in _LoginPartial.cshtml, I can only see that it contains default Key=Title, Value=Home Page. What should I do to have MyKey available in _LoginPartial?
Thanks!
The problem is that you are trying to set ViewData content within the controller’s constructor.
The ViewData dictionary is not actually created by the controller. It is created at a very different point within the MVC pipeline and then gets injected into the controller. You can basically see this process as something like this:
// create controller
var controller = CreateController<MyController>();
// do stuff
// inject ViewData
controller.ViewData = GetViewDataDictionary();
// invoke controller action
var result = controller.SomeAction();
So the ViewData gets provided after the controller has been created; after its constructor ran. So any assignments to the view data within the constructor will not apply to the actual view data dictionary.
As such, you will need to set those values at a different time, you cannot use the constructor there. Using the ViewData in general is somewhat legacy construct that you should try to avoid if possible. Instead, you should work with strongly typed view model objects. Of course, this will require you to pass the data explicitly in each action, but that way you are also not introducing any implicit data flow.
An alternative, which is especially useful if what you are doing should actually always apply to the _LoginPartial, would be to use a view component. View components are reusable components that you can use inside of your views, which will behave similarly to a controller action. So you could just insert a view component into your partial, and have that run the logic (asynchronously even!) to provide the data from your database. And you wouldn’t need to mess with any of your controllers to make it work.
ViewData can be accessed after controller has been activated or by overrinding OnActionExecuting.
//
// Summary:
// Gets or sets Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary used by
// Microsoft.AspNetCore.Mvc.ViewResult and Microsoft.AspNetCore.Mvc.Controller.ViewBag.
//
// Remarks:
// By default, this property is intiailized when Microsoft.AspNetCore.Mvc.Controllers.IControllerActivator
// activates controllers.
// This property can be accessed after the controller has been activated, for example,
// in a controller action or by overriding Microsoft.AspNetCore.Mvc.Controller.OnActionExecuting(Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext).
// This property can be also accessed from within a unit test where it is initialized
// with Microsoft.AspNetCore.Mvc.ModelBinding.EmptyModelMetadataProvider.
[ViewDataDictionary]
public ViewDataDictionary ViewData { get; set; }
For a solution, you could try overring OnActionExecuting like below:
public class BaseController : Controller
{
public ApplicationDbContext _db;
public BaseController(ApplicationDbContext db)
{
_db = db;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
ViewData["MyKey"] = _db.Product.ToList();
base.OnActionExecuting(context);
}
}
_LoginPartial.cshtml
#foreach (Product item in #ViewData["MyKey"] as IList<Product>)
{
<li><a>#item.Name</a></li>
}
I have MVC5 app which has 2 areas: area1 and area2.
Some views in those areas are using code from controller which is actually same for view in area1 and view in area2.
Now I have 2 controllers per each area, but as I mentioned, the code is very same.
How can I use one Controller per each view in the area 1 and 2 to avoid code duplicity and have simpler maintainability?
Areas are just a layer, but they can still interact with each other and the application at large. If you need to share a controller, you can simply subclass it. Better yet, create an abstract controller outside of the areas and inherit each area's controller from that.
As far as views go, Razor has a very easy convention for handling overrides and fallbacks. It searches multiple directories, based on convention, for the required view and stops only when it finds a match.
For example, if you placed the view in Views\Shared, the last resort fallback, it can be used literally anywhere in your application, including each of your areas. The order of ops for view location is:
Areas\[AreaName]\Views\[ControllerName]
Areas\[AreaName]\Views\Shared
Views\[ControllerName]
Views\Shared
Razor will go down the list looking for the view in each location until it finds it.
You keep the separate controllers as they serve their purposes in the MVC framework. However, you can export much of the code in the controllers to service classes and each controller uses the services as needed -- Now you avoid code duplicity and have simpler maintainability.
Controllers
public class HomeController : Controller
{
private IFooService service;
public HomeController()
{
this.service = new FooService(dbContext);
}
public ActionResult CalculateFoo(int id)
{
var foo = this.service.CalculateFoo(id);
return View(foo);
}
}
public class FooController : Controller
{
private IFooService service;
public FooController()
{
this.service = new FooService(dbContext);
}
public ActionResult Details(int id)
{
var foo = this.service.CalculateFoo(id);
return View(foo);
}
}
Service
public class FooService : IFooService
{
private DbContext db;
public FooService(DbContext db)
{
this.db = db;
}
public Foo CalculateFoo(int id)
{
var foo = this.db.Foo.First(f => f.id == id);
// do stuff
return foo;
}
}
Maintenance Edit
After using this approach for a while I found myself only adding the exact same boilerplate code in every controller so I decided to do some reflection magic. In the meantime I ditched using MVC for my views - Razor is just so tedious and ugly - so I basically use my handlers as a JSON backend. The approach I currently use is to decorate my queries/commands with a Route attribute that is located in some common assembly like this:
[Route("items/add", RouteMethod.Post)]
public class AddItemCommand { public Guid Id { get; set; } }
[Route("items", RouteMethod.Get)]
public class GetItemsQuery : IQuery<GetItemsResponse> { }
// The response inherits from a base type that handles
// validation messages and the like
public class GetItemsResponse : ServiceResponse { }
I then implemented an MVC host that extracts the annotated commands/queries and generates the controllers and handlers for me at startup time. With this my application logic is finally free of MVC cruft. The query responses are also automatically populated with validation messages. My MVC applications now all look like this:
+ MvcApp
+- Global.asax
+- Global.asax.cs - Startup the host and done
+- Web.config
After realizing I really don't use MVC outside the host - and constantly having issues with the bazillion dependencies the framework has - I implemented another host based on NServiceKit. Nothing had to be changed in my application logic and the dependencies are down to System.Web, NServiceKit and NServiceKit.Text that takes good care of the model binding. I know it's a very similar approach to how NServiceKit/ServiceStack does their stuff but I'm now totally decoupled from the web framework in use so in case a better one comes along I just implement another host and that's it.
The situation
I'm currently working on an ASP.NET MVC site that's implementing the businesslogic-view separation via the IQueryHandler and ICommandHandler abstractions (using the almighty SimpleInjector for dependency injection).
The Problem
I've got to attach some custom validation logic to a QueryHandler via a decorator and that's working pretty well in and of itself. The problem is that in the event of validation errors I want to be able to show the same view that the action would have returned but with information on the validation error of course. Here is a sample for my case:
public class HomeController : Controller
{
private readonly IQueryHandler<SomeQuery, SomeTransport> queryHandler;
public ActionResult Index()
{
try
{
var dto = this.queryHandler.Handle(new SomeQuery { /* ... */ });
// Doing something awesome with the data ...
return this.View(new HomeViewModel());
}
catch (ValidationException exception)
{
this.ModelState.AddModelErrors(exception);
return this.View(new HomeViewModel());
}
}
}
In this scenario I have some business logic that's handled by the queryHandler that is decorated with a ValidationQueryHandlerDecorator that throws ValidationExceptions when it is appropriate.
What I want it to do
What I want is something along the lines of:
public class HomeController : Controller
{
private readonly IQueryHandler<SomeQuery, SomeTransport> queryHandler;
public ActionResult Index()
{
var dto = this.queryHandler.Handle(new SomeQuery { /* ... */ });
// Doing something awesome with the data ...
// There is a catch-all in place for unexpected exceptions but
// for ValidationExceptions I want to do essentially the same
// view instantiation but with the model errors attached
return this.View(new HomeViewModel());
}
}
I've been thinking about a special ValidationErrorHandlerAttribute but then I'm losing the context and I can't really return the proper view. The same goes with the approach where I just wrap the IQueryHandler<,> with a decorator... I've seen some strange pieces of code that did some string sniffing on the route and then instantiating a new controller and viewmodel via Activator.CreateInstance - that doesn't seem like a good idea.
So I'm wondering whether there is a nice way to do this ... maybe I just don't see the wood from the trees. Thanks!
I don't think there's a way to make the action method oblivious to this, since the action method is in control of the returned view model, and in case of a validation exception you need to return a view model with all the actual data (to prevent the user from losing his changes). What you might be able to do however to make this more convenient is add an extension method for executing queries in an action:
public ActionResult Index()
{
var result = this.queryHandler.ValidatedHandle(this.ModelState, new SomeQuery { });
if (result.IsValid) {
return this.View(new HomeViewModel(result.Data));
}
else
{
return this.View(new HomeViewModel());
}
}
The ValidatedHandle extension method could look like this:
public static ValidatedResult<TResult> ValidatedHandle<TQuery, TResult>(
this IQueryHandler<TQuery, TResult> handler,
TQuery query, ModelStateDictionary modelState)
{
try
{
return new ValidatedResult<TResult>.CreateValid(handler.Handle(query));
}
catch (ValidationException ex)
{
modelState.AddModelErrors(ex);
return ValidatedResult<TResult>.Invalid;
}
}
Do note that you should only catch such validation exception if the validation is on data that the user has entered. If you send a query with parameters that are set programmatically, a validation exception simply means a programming error and you should blog up, log the exception and show a friendly error page to the user.
Since my #html.render action crashes my dev and prod servers i have to use partials(crap).
I tried creating public partial controller{} class so i can set needed data for all my views but i am having no luck (everything breaks).
I am coming from LAMP cakePHP background and really need simplicity.
I need to know how to create a partial base controller(that doesnt override the regular base controller) and how to access multiple models from the class.
Thank you!
public class BaseController: Controller
{
public override OnActionExecuting(...) { ... }
public override OnActionExecuted(... context)
{
if (context.Result is ViewResult)
((ViewResult)context.Result).ViewData["mycommondata"] = data;
}
...
}
public class MyController1: BaseController
{
}
I.e. just derive from your new base controller class.
However I'd suggest you to ask here why your RenderPartial "crashes" - since it can be a better way for you, and it obviously shouldn't crash.
better way to create base controller
public class Controller : System.Web.Mvc.Controller
{
public shipsEntities db = new shipsEntities();
public Controller()
{
ViewData["ships"] = db.ships.ToList();
}
}
that way the rest of controllers follow regular convention
public class MyController : Controller